├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Taskfile.yml ├── composer.json ├── kdl.svg ├── phpbench.json ├── phpcs.xml ├── phpunit.xml ├── psalm.xml ├── src ├── Document.php ├── Kdl.php ├── Node.php ├── NodeInterface.php ├── ParseException.php ├── Parser.php ├── grammar.php └── helpers.php └── tests ├── DocumentTest.php ├── GrammarTest.php ├── NodeTest.php ├── ParserTest.php ├── bench └── ParseBench.php ├── kdl ├── ci.kdl ├── nodes_1.kdl ├── nodes_2.kdl ├── nodes_3.kdl ├── nodes_4.kdl ├── nodes_5.kdl ├── nodes_6.kdl ├── nodes_7.kdl ├── nodes_8.kdl ├── nodes_9.kdl ├── value_number_decimal.kdl ├── value_number_other.kdl ├── value_number_separator.kdl ├── value_other.kdl └── website.kdl └── suite.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Project checks 2 | 3 | on: push 4 | 5 | jobs: 6 | checks: 7 | name: Project checks 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out code 11 | uses: actions/checkout@v2 12 | - name: Set up PHP 13 | uses: shivammathur/setup-php@v2 14 | with: 15 | php-version: 7.4 16 | tools: composer:v2 17 | - name: Install PHP lib dependencies 18 | run: composer install 19 | - name: Install task runner 20 | run: sudo sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin 21 | - name: Run checks 22 | run: task checks 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | .phpunit.result.cache 3 | vendor/ 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.4.0] - 2021-05-30 8 | 9 | ### Changed 10 | - add a benchmark task in dev environment (`task bench`) using [PHPBench](https://phpbench.readthedocs.io/) 11 | - use Parsica release 0.8.1, giving an approx 2x speed-up vs previous release! 12 | 13 | ## [0.3.0] - 2021-02-24 14 | 15 | ### Changed 16 | - using Parsica release 0.7, with new `Parsica` PHP namespace and new package name (`parsica-php/parsica`) 17 | 18 | ## [0.2.0] - 2021-01-13 19 | 20 | ### Added 21 | - clear memoised parsers at end of parse operation 22 | 23 | ### Changed 24 | - adopted `Kdl` vendor PHP namespace (as per [PSR-4](https://www.php-fig.org/psr/psr-4/#2-specification)) as repository passed to kdl-org ownership, and `kdl/kdl` as package name for e.g. Packagist 25 | 26 | ## [0.1.1] - 2021-01-09 27 | 28 | ### Added 29 | - internal memoisation of instantiated Parsica parser objects, giving ~3% speed-up 30 | 31 | ## [0.1.0] - 2021-01-08 32 | 33 | ### Added 34 | - initial implementation using [Parsica](https://parsica.verraes.net), passing tests migrated from both JS and Rust libraries -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2020 Douglas Greenshields 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Stable Version](https://poser.pugx.org/kdl/kdl/v)](//packagist.org/packages/kdl/kdl) [![License](https://poser.pugx.org/kdl/kdl/license)](//packagist.org/packages/kdl/kdl) ![Status](https://github.com/kdl-org/kdl-php/workflows/Project%20checks/badge.svg?branch=main) 2 | 3 | # KDL-PHP 4 | 5 | A PHP library for the [KDL Document Language](https://kdl.dev) (pronounced like "cuddle"). 6 | 7 | ![alt text](./kdl.svg "KDL logo") 8 | 9 | ## Installation 10 | 11 | The KDL library can be included to your project using Composer: 12 | 13 | `composer require kdl/kdl` 14 | 15 | ## Limitations 16 | 17 | For now, this library only supports parsing. 18 | 19 | Parsing a 45 line file with a tree depth of 5 is likely to take about 30ms currently - this speed is improving all the time with releases of the underlying parser library. This library favours correctness over performance - however, the aim is for parse speed to improve to a point of being reasonable. 20 | 21 | The parser uses [Parsica](https://parsica.verraes.net/) as an underlying parsing library in order to map fairly directly and clearly onto the published KDL grammar - Parsica uses FP principles, and one result of this is that the call stack depth used during parsing may be high. Be warned if you are using e.g. xdebug, as parsing may exceed any normal maximum stack depth that you may set. 22 | 23 | ## Examples 24 | 25 | ```php 26 | $document = Kdl\Kdl\Kdl::parse($kdlString); 27 | foreach ($document as $node) { 28 | $node->getName(); //gets the name for the node @see https://github.com/kdl-org/kdl/blob/main/SPEC.md#node 29 | $node->getValues(); //gets a list of values for a node @see https://github.com/kdl-org/kdl/blob/main/SPEC.md#value 30 | $node->getProperties(); //gets an associative array of properties @see https://github.com/kdl-org/kdl/blob/main/SPEC.md#property 31 | foreach ($node->getChildren() as $child) { 32 | $child->getName(); //name for the child 33 | //etc 34 | } 35 | } 36 | ``` 37 | 38 | ## License 39 | 40 | The code is available under the [MIT license](LICENSE). -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | unit: 5 | cmds: 6 | - vendor/bin/phpunit 7 | sa: 8 | cmds: 9 | - vendor/bin/psalm 10 | sa-info: 11 | cmds: 12 | - vendor/bin/psalm --show-info=true 13 | cs: 14 | cmds: 15 | - vendor/bin/phpcs 16 | csfix: 17 | cmds: 18 | - vendor/bin/phpcbf 19 | bench: 20 | cmds: 21 | - vendor/bin/phpbench run tests/bench --report=default 22 | checks: 23 | deps: [ unit, cs, sa ] 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kdl/kdl", 3 | "description": "A PHP implementation for the KDL data format", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Douglas Greenshields", 9 | "email": "dgreenshields@gmail.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "Kdl\\Kdl\\": "src" 15 | }, 16 | "files": [ 17 | "src/grammar.php", 18 | "src/helpers.php" 19 | ] 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "Kdl\\Kdl\\Tests\\": "tests" 24 | } 25 | }, 26 | "require": { 27 | "php": ">=7.4", 28 | "parsica-php/parsica": "^0.8.1" 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": "^9.5.0", 32 | "vimeo/psalm": "^4.3.2", 33 | "squizlabs/php_codesniffer": "^3.5.8", 34 | "ext-json": "*", 35 | "phpbench/phpbench": "^1.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /kdl.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /phpbench.json: -------------------------------------------------------------------------------- 1 | { 2 | "runner.bootstrap": "vendor/autoload.php" 3 | } 4 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | src 4 | tests 5 | vendor/* 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src/ 6 | 7 | 8 | 9 | 10 | tests 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Document.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | private array $nodes; 16 | 17 | public function __construct(array $nodes) 18 | { 19 | $this->nodes = $nodes; 20 | } 21 | 22 | /** 23 | * @return NodeInterface[] 24 | */ 25 | public function getNodes(): array 26 | { 27 | return $this->nodes; 28 | } 29 | 30 | public function jsonSerialize(): array 31 | { 32 | return array_map( 33 | static function (NodeInterface $node): array { 34 | return $node->jsonSerialize(); 35 | }, 36 | $this->getNodes() 37 | ); 38 | } 39 | 40 | public function getIterator(): \Traversable 41 | { 42 | return new \ArrayIterator($this->getNodes()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Kdl.php: -------------------------------------------------------------------------------- 1 | parse($kdl); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Node.php: -------------------------------------------------------------------------------- 1 | name = $name; 15 | $this->values = $values; 16 | $this->properties = $properties; 17 | $this->children = []; 18 | } 19 | 20 | public function getName(): string 21 | { 22 | return $this->name; 23 | } 24 | 25 | public function getValues(): array 26 | { 27 | return $this->values; 28 | } 29 | 30 | public function getProperties(): array 31 | { 32 | return $this->properties; 33 | } 34 | 35 | public function getChildren(): array 36 | { 37 | return $this->children; 38 | } 39 | 40 | public function attachChild(Node $child): void 41 | { 42 | $this->children[] = $child; 43 | } 44 | 45 | public function jsonSerialize(): array 46 | { 47 | return self::serialize($this); 48 | } 49 | 50 | private static function serialize(NodeInterface $node): array 51 | { 52 | return [ 53 | 'name' => $node->getName(), 54 | 'values' => $node->getValues(), 55 | 'properties' => (object)$node->getProperties(), 56 | 'children' => array_map( 57 | static function (NodeInterface $child): array { 58 | return self::serialize($child); 59 | }, 60 | $node->getChildren() 61 | ), 62 | ]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/NodeInterface.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public function getChildren(): array; 19 | } 20 | -------------------------------------------------------------------------------- /src/ParseException.php: -------------------------------------------------------------------------------- 1 | thenEof()->tryString($kdl)->output(); 21 | } catch (ParserHasFailed $e) { 22 | throw new ParseException( 23 | $e->parseResult()->errorMessage(), 24 | 0, 25 | $e, 26 | ); 27 | } finally { 28 | clearMemo(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/grammar.php: -------------------------------------------------------------------------------- 1 | recurse( 48 | between( 49 | zeroOrMore(linespace()), 50 | zeroOrMore(linespace()), 51 | optional( 52 | collect($nodeParser, optional($nodesParser)) 53 | ->map(fn($collection) => array_merge([$collection[0]], $collection[1] ?? [])) 54 | ), 55 | ) 56 | ); 57 | $nodesParser 58 | ->label('nodes'); 59 | 60 | $nodeParser->recurse( 61 | collect( 62 | sequence( 63 | optional(assemble(string("/-"), zeroOrMore(ws()))), 64 | identifier(), 65 | ), 66 | optional(nodeSpace()), 67 | sepBy( 68 | nodeSpace(), 69 | nodePropsAndArgs(), 70 | )->map( 71 | function (?array $propsAndArgs): array { 72 | return [ 73 | 'values' => array_merge( 74 | ...array_map( 75 | fn(array $struct) => $struct['values'], 76 | array_filter( 77 | $propsAndArgs ?? [], 78 | fn(array $struct) => array_key_exists('values', $struct) 79 | ), 80 | ) 81 | ), 82 | 'properties' => array_merge( 83 | ...array_map( 84 | fn(array $struct) => $struct['properties'], 85 | array_filter( 86 | $propsAndArgs ?? [], 87 | fn(array $struct) => array_key_exists('properties', $struct), 88 | ), 89 | ) 90 | ), 91 | ]; 92 | } 93 | ), 94 | keepFirst( 95 | optional(between(zeroOrMore(nodeSpace()), zeroOrMore(ws()), $nodeChildrenParser)), 96 | nodeTerminator(), 97 | ), 98 | )->map(function (array $collection): NodeInterface { 99 | [$identifier, , $nodePropsAndArgs, $children] = $collection; 100 | $node = new Node( 101 | $identifier, 102 | $nodePropsAndArgs['values'] ?? [], 103 | $nodePropsAndArgs['properties'] ?? [], 104 | ); 105 | if (is_array($children)) { 106 | foreach ($children as $child) { 107 | $node->attachChild($child); 108 | } 109 | } 110 | 111 | return $node; 112 | }) 113 | ); 114 | $nodeParser->label('node'); 115 | 116 | $nodeChildrenParser->recurse( 117 | collect( 118 | optional(assemble(string("/-"), zeroOrMore(ws()))) 119 | ->map(fn($x) => (bool) $x), 120 | between(char('{'), char('}'), $nodesParser) 121 | )->map(function (array $args) { 122 | [$isSlashDashComment, $nodes] = $args; 123 | 124 | return (!$isSlashDashComment) ? $nodes : null; 125 | }) 126 | ); 127 | $nodeChildrenParser->label('node-children'); 128 | 129 | return $nodesParser 130 | ->map(fn($nodes) => new Document($nodes ?: [])); 131 | } 132 | 133 | function nodePropsAndArgs(): Parser 134 | { 135 | return memo( 136 | __FUNCTION__, 137 | fn() => collect( 138 | optional(assemble(string("/-"), zeroOrMore(ws()))) 139 | ->map(fn($x) => (bool) $x), 140 | either(prop(), value()) 141 | ) 142 | ->map(function (array $args) { 143 | [$isSlashDashComment, $propOrArg] = $args; 144 | 145 | return (!$isSlashDashComment) ? $propOrArg : []; 146 | }) 147 | ->label('node-props-and-args') 148 | ); 149 | } 150 | 151 | function nodeSpace(): Parser 152 | { 153 | return memo( 154 | __FUNCTION__, 155 | fn() => either(assemble(zeroOrMore(ws()), escline(), zeroOrMore(ws())), atLeastOne(ws())) 156 | ->map(fn($x) => null) 157 | ->label('node-space') 158 | ); 159 | } 160 | 161 | function nodeTerminator(): Parser 162 | { 163 | return memo( 164 | __FUNCTION__, 165 | fn() => choice(singleLineComment(), newline(), char(';'), eof()) 166 | ->label('node-terminator') 167 | ); 168 | } 169 | 170 | function identifier(): Parser 171 | { 172 | return memo( 173 | __FUNCTION__, 174 | fn() => either(string_(), bareIdentifier()) 175 | ->label('identifier') 176 | ); 177 | } 178 | 179 | function bareIdentifier(): Parser 180 | { 181 | return memo( 182 | __FUNCTION__, 183 | fn() => assemble(either(digitChar(), identifierChar()), zeroOrMore(identifierChar())) 184 | ->label('bare-identifier') 185 | ); 186 | } 187 | 188 | function identifierChar(): Parser 189 | { 190 | return memo( 191 | __FUNCTION__, 192 | fn() => satisfy(orPred( 193 | isCharCode(array_merge( 194 | range(0x21, 0x3A), 195 | range(0x3F, 0x5A), 196 | range(0x5E, 0x7A), 197 | [0x7C], 198 | )), 199 | isCharBetween(0x7E, 0xFFFF), 200 | )) 201 | ->label('identifier-char') 202 | ); 203 | } 204 | 205 | function prop(): Parser 206 | { 207 | return memo( 208 | __FUNCTION__, 209 | fn() => collect(identifier(), char('='), value()) 210 | ->map(fn(array $collected) => ['properties' => [$collected[0] => $collected[2]['values'][0]]]) 211 | ->label('prop') 212 | ); 213 | } 214 | 215 | function value(): Parser 216 | { 217 | return memo( 218 | __FUNCTION__, 219 | fn() => choice(string_(), number(), boolean(), string("null")->map(fn() => null)) 220 | ->map(fn($value) => ['values' => [$value]]) 221 | ->label('value') 222 | ); 223 | } 224 | 225 | function string_(): Parser 226 | { 227 | return memo( 228 | __FUNCTION__, 229 | fn() => either(rawString(), escapedString()) 230 | ->label('string') 231 | ); 232 | } 233 | 234 | function escapedString(): Parser 235 | { 236 | return memo( 237 | __FUNCTION__, 238 | fn() => between(char('"'), char('"'), zeroOrMore(character())) 239 | ->label('escaped-string') 240 | ); 241 | } 242 | 243 | function character(): Parser 244 | { 245 | return memo( 246 | __FUNCTION__, 247 | fn() => either(keepSecond(char('\\'), escape()), noneOfS("\\\"")) 248 | ->label('character') 249 | ); 250 | } 251 | 252 | function escape(): Parser 253 | { 254 | return memo( 255 | __FUNCTION__, 256 | fn() => either( 257 | oneOfS('"\\/bfnrt') 258 | ->map(function (string $val): string { 259 | $escapes = [ 260 | 'b' => "\u{08}", 261 | 'f' => "\u{0C}", 262 | 'n' => "\n", 263 | 'r' => "\r", 264 | 't' => "\t", 265 | ]; 266 | return $escapes[$val] ?? $val; 267 | }), 268 | assemble(string("u{"), repeat(6, optional(hexDigitChar())), char('}')) 269 | ->map(fn($val): string => '\\' . $val), 270 | ) 271 | ->label('escape') 272 | ); 273 | } 274 | 275 | function rawString(): Parser 276 | { 277 | return memo( 278 | __FUNCTION__, 279 | fn() => keepSecond(char('r'), rawStringHash()) 280 | ->label('raw-string') 281 | ); 282 | } 283 | 284 | function rawStringHash(): Parser 285 | { 286 | return memo( 287 | __FUNCTION__, 288 | function () { 289 | $hashParser = recursive(); 290 | $hashParser->recurse( 291 | either(between(char('#'), char('#'), $hashParser), rawStringQuotes()) 292 | ); 293 | 294 | return $hashParser 295 | ->label('raw-string-hash'); 296 | } 297 | ); 298 | } 299 | 300 | function rawStringQuotes(): Parser 301 | { 302 | return memo( 303 | __FUNCTION__, 304 | fn() => between(char('"'), char('"'), zeroOrMore(anySingleBut('"'))) 305 | ->label('raw-string-quotes') 306 | ); 307 | } 308 | 309 | function number(): Parser 310 | { 311 | return memo( 312 | __FUNCTION__, 313 | fn() => choice(hex(), octal(), binary(), decimal()) 314 | ->label('number') 315 | ); 316 | } 317 | 318 | function decimal(): Parser 319 | { 320 | return memo( 321 | __FUNCTION__, 322 | fn() => collect( 323 | integer(), 324 | optional(assemble(char('.'), digitChar(), zeroOrMore(either(digitChar(), char('_'))))), 325 | optional(exponent()), 326 | ) 327 | ->map(function (array $collection) { 328 | [$integer, $fraction, $exponent] = $collection; 329 | $useInt = $fraction === null && $exponent === null; 330 | $assembly = implode('', array_filter( 331 | [ 332 | $integer, 333 | $fraction ? str_replace('_', '', $fraction) : null, 334 | $exponent, 335 | ], 336 | fn($x): bool => $x !== null, 337 | )); 338 | return $useInt ? (int) $assembly : (float) $assembly; 339 | }) 340 | ->label('decimal') 341 | ); 342 | } 343 | 344 | function exponent(): Parser 345 | { 346 | return memo( 347 | __FUNCTION__, 348 | fn() => assemble(either(char('e'), char('E')), integer()) 349 | ->label('exponent') 350 | ); 351 | } 352 | 353 | function integer(): Parser 354 | { 355 | return memo( 356 | __FUNCTION__, 357 | fn() => assemble(optional(sign()), digitChar(), zeroOrMore(either(digitChar(), char('_')))) 358 | ->map(fn($x) => str_replace('_', '', $x)) 359 | ->label('integer') 360 | ); 361 | } 362 | 363 | function sign(): Parser 364 | { 365 | return memo( 366 | __FUNCTION__, 367 | fn() => either(char('+'), char('-')) 368 | ->label('sign') 369 | ); 370 | } 371 | 372 | function hex(): Parser 373 | { 374 | return memo( 375 | __FUNCTION__, 376 | fn() => keepSecond(string("0x"), assemble(hexDigitChar(), zeroOrMore(either(hexDigitChar(), char('_'))))) 377 | ->map(fn(string $x) => hexdec(str_replace('_', '', $x))) 378 | ->label('hex') 379 | ); 380 | } 381 | 382 | function octal(): Parser 383 | { 384 | return memo( 385 | __FUNCTION__, 386 | fn() => keepSecond(string("0o"), assemble(octDigitChar(), zeroOrMore(either(octDigitChar(), char('_'))))) 387 | ->map(fn(string $x) => octdec(str_replace('_', '', $x))) 388 | ->label('octal') 389 | ); 390 | } 391 | 392 | function binary(): Parser 393 | { 394 | return memo( 395 | __FUNCTION__, 396 | fn() => keepSecond(string("0b"), assemble(binDigitChar(), zeroOrMore(either(binDigitChar(), char('_'))))) 397 | ->map(fn(string $x) => bindec(str_replace('_', '', $x))) 398 | ->label('binary') 399 | ); 400 | } 401 | 402 | function boolean(): Parser 403 | { 404 | return memo( 405 | __FUNCTION__, 406 | fn() => either(string("true"), string("false")) 407 | ->map(fn(string $val) => $val === "true") 408 | ->label('boolean') 409 | ); 410 | } 411 | 412 | function escline(): Parser 413 | { 414 | return memo( 415 | __FUNCTION__, 416 | fn() => assemble(string('\\'), zeroOrMore(ws()), either(singleLineComment(), newline())) 417 | ->label('escline') 418 | ); 419 | } 420 | 421 | function linespace(): Parser 422 | { 423 | return memo( 424 | __FUNCTION__, 425 | fn() => choice(newline(), ws(), singleLineComment()) 426 | ->label('linespace') 427 | ); 428 | } 429 | 430 | function newline(): Parser 431 | { 432 | return memo( 433 | __FUNCTION__, 434 | fn() => either( 435 | crlf(), 436 | satisfy(isCharCode([ 437 | 0x0D, 438 | 0x0A, 439 | 0x85, 440 | 0x0C, 441 | 0x2028, 442 | 0x2029, 443 | ])) 444 | ) 445 | ); 446 | } 447 | 448 | function notNewline(): Parser 449 | { 450 | //constructing the complement of newline() 451 | return memo( 452 | __FUNCTION__, 453 | fn() => satisfy( 454 | notPred(isCharCode([ 455 | 0x0D, 456 | 0x0A, 457 | 0x85, 458 | 0x0C, 459 | 0x2028, 460 | 0x2029, 461 | ])), 462 | ) 463 | ); 464 | } 465 | 466 | function ws(): Parser 467 | { 468 | return memo( 469 | __FUNCTION__, 470 | fn() => choice( 471 | bom(), 472 | unicodeSpace(), 473 | multiLineComment() 474 | ) 475 | ->label('ws') 476 | ); 477 | } 478 | 479 | function bom(): Parser 480 | { 481 | return memo( 482 | __FUNCTION__, 483 | fn() => char("\u{FFEF}") 484 | ->label('bom') 485 | ); 486 | } 487 | 488 | function unicodeSpace(): Parser 489 | { 490 | return memo( 491 | __FUNCTION__, 492 | fn() => satisfy(isCharCode([ 493 | 0x09, 494 | 0x20, 495 | 0xA0, 496 | 0x1680, 497 | 0x2000, 498 | 0x2001, 499 | 0x2002, 500 | 0x2003, 501 | 0x2004, 502 | 0x2005, 503 | 0x2006, 504 | 0x2007, 505 | 0x2008, 506 | 0x2009, 507 | 0x200A, 508 | 0x202F, 509 | 0x205F, 510 | 0x3000, 511 | ]))->label('whitespace (unicode-space)') 512 | ); 513 | } 514 | 515 | function singleLineComment(): Parser 516 | { 517 | return memo( 518 | __FUNCTION__, 519 | fn() => between(string("//"), either(newline(), eof()), atLeastOne(notNewline())) 520 | ->label('single-line-comment') 521 | ); 522 | } 523 | 524 | function multiLineComment(): Parser 525 | { 526 | return memo( 527 | __FUNCTION__, 528 | fn() => between( 529 | string("/*"), 530 | string("*/"), 531 | zeroOrMore(either(anySingleBut('*'), char('*')->notFollowedBy(char('/')))), 532 | ) 533 | ->label('multi-line-comment') 534 | ); 535 | } 536 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | $from <= mb_ord($x) && mb_ord($x) <= $to; 18 | } 19 | 20 | function memo(string $name, callable $parserFactory): Parser 21 | { 22 | $stored = _memo($name); 23 | if ($stored === null) { 24 | $stored = $parserFactory(); 25 | _memo($name, $stored); 26 | } 27 | 28 | return $stored; 29 | } 30 | 31 | function _memo(?string $name, ?Parser $parser = null, bool $justClear = false) 32 | { 33 | static $memo = []; 34 | if ($justClear) { 35 | $memo = []; 36 | 37 | return null; 38 | } 39 | if ($name === null) { 40 | throw new \InvalidArgumentException(); 41 | } 42 | if ($parser === null) { 43 | return $memo[$name] ?? null; 44 | } 45 | 46 | $memo[$name] = $parser; 47 | 48 | return null; 49 | } 50 | 51 | function clearMemo(): void 52 | { 53 | _memo(null, null, true); 54 | } 55 | -------------------------------------------------------------------------------- /tests/DocumentTest.php: -------------------------------------------------------------------------------- 1 | createMock(NodeInterface::class)]; 14 | $document = new Document($nodes); 15 | self::assertSame($nodes, iterator_to_array($document)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/GrammarTest.php: -------------------------------------------------------------------------------- 1 | makeAssertionsForParser($input, $output, string_()); 35 | } 36 | 37 | public function strings(): array 38 | { 39 | return [ 40 | //escaped strings 41 | ["\"\"", ""], 42 | ["\"hello\"", "hello"], 43 | ["\"hello\nworld\"", "hello\nworld"], 44 | ["\"\u{10FFF}\"", "\u{10FFF}"], 45 | [<<<'EOT' 46 | "\"\\\/\b\f\n\r\t" 47 | EOT 48 | ,"\"\\/\u{08}\u{0C}\n\r\t"], 49 | ['"\\u{10}"', '\u{10}'], 50 | ['"\\i"', self::ERROR], 51 | // ['"\\u{c0ffee}"', self::ERROR], // src/parser.rs:791 from kdl-rs references this... 52 | //raw strings 53 | ['r"foo"', 'foo'], 54 | ["r\"foo\nbar\"", "foo\nbar"], 55 | ['r#"foo"#', 'foo'], 56 | ['r##"foo"##', 'foo'], 57 | ['r"\nfoo\r"', '\nfoo\r'], 58 | ['r##"foo"#', self::ERROR], 59 | ]; 60 | } 61 | 62 | /** 63 | * @dataProvider numbers 64 | */ 65 | public function testNumber(string $input, $output): void 66 | { 67 | $this->makeAssertionsForParser($input, $output, number()); 68 | } 69 | 70 | public function numbers(): array 71 | { 72 | return [ 73 | //floats 74 | ['1.0', 1.0], 75 | ['0.0', 0.0], 76 | ['-1.0', -1.0], 77 | ['+1.0', 1.0], 78 | ['1.0e10', 1.0e10], 79 | ['1.0e-10', 1.0e-10], 80 | ['-1.0e-10', -1.0e-10], 81 | ['123_456_789.0', 123456789.0], 82 | ['123_456_789.0_', 123456789.0], 83 | ['?1.0', self::ERROR], 84 | ['_1.0', self::ERROR], 85 | ['1._0', self::ERROR], 86 | ['1.', self::ERROR], 87 | ['.0', self::ERROR], 88 | //integers 89 | ['0', 0], 90 | ['0123456789', 123456789], 91 | ['0123_456_789', 123456789], 92 | ['0123_456_789_', 123456789], 93 | ['+0123456789', 123456789], 94 | ['-0123456789', -123456789], 95 | ['?0123456789', self::ERROR], 96 | ['_0123456789', self::ERROR], 97 | ['a', self::ERROR], 98 | ['--', self::ERROR], 99 | //hexadecimal 100 | ['0x0123456789abcdef', 0x0123456789abcdef], 101 | ['0x01234567_89abcdef', 0x0123456789abcdef], 102 | ['0x01234567_89abcdef_', 0x0123456789abcdef], 103 | ['0x_123', self::ERROR], 104 | ['0xg', self::ERROR], 105 | ['0xx', self::ERROR], 106 | //octal 107 | ['0o01234567', 001234567], 108 | ['0o0123_4567', 001234567], 109 | ['0o01234567_', 001234567], 110 | //binary 111 | ['0b0101', 0b0101], 112 | ['0b01_10', 0b110], 113 | ['0b01___10', 0b110], 114 | ['0b0110_', 0b110], 115 | ['0b_0110', self::ERROR], 116 | ['0b20', self::ERROR], 117 | ['0bb', self::ERROR], 118 | ]; 119 | } 120 | 121 | /** 122 | * @dataProvider booleans 123 | */ 124 | public function testBooleans(string $input, $output): void 125 | { 126 | $this->makeAssertionsForParser($input, $output, boolean()); 127 | } 128 | 129 | public function booleans(): array 130 | { 131 | return [ 132 | ['true', true], 133 | ['false', false], 134 | ['blah', self::ERROR], 135 | ]; 136 | } 137 | 138 | /** 139 | * @dataProvider nodeSpaces 140 | * @param string $input 141 | * @param $output 142 | */ 143 | public function testNodeSpaces(string $input, $output): void 144 | { 145 | $this->makeAssertionsForParser($input, $output, nodeSpace()); 146 | } 147 | 148 | public function nodeSpaces(): array 149 | { 150 | return [ 151 | [' ', ''], 152 | ["\t ", ''], 153 | ["\t \\ // hello\n ", ''], 154 | ['blah', self::ERROR] 155 | ]; 156 | } 157 | 158 | /** 159 | * @dataProvider singleLineComments 160 | * @param string $input 161 | * @param string $remainder 162 | */ 163 | public function testSingleLineComments(string $input, string $remainder): void 164 | { 165 | self::assertEquals($remainder, (string) singleLineComment()->tryString($input)->remainder()); 166 | } 167 | 168 | public function singleLineComments(): array 169 | { 170 | return [ 171 | ['//hello', ''], 172 | ["// \thello", ''], 173 | ["//hello\n", ''], 174 | ["//hello\r\n", ''], 175 | ["//hello\n\r", "\r"], 176 | ["//hello\rworld", 'world'], 177 | ["//hello\nworld\r\n", "world\r\n"], 178 | ]; 179 | } 180 | 181 | /** 182 | * @dataProvider multiLineComments 183 | * @param string $input 184 | * @param string $remainder 185 | */ 186 | public function testMultiLineComments(string $input, string $remainder): void 187 | { 188 | self::assertEquals($remainder, (string) multiLineComment()->tryString($input)->remainder()); 189 | } 190 | 191 | public function multiLineComments(): array 192 | { 193 | return [ 194 | ["/*hello*/", ''], 195 | ["/*hello*/\n", "\n"], 196 | ["/*\nhello\r\n*/", ''], 197 | ["/*\nhello** /\n*/", ''], 198 | ["/**\nhello** /\n*/", ''], 199 | ["/*hello*/world", 'world'], 200 | ]; 201 | } 202 | 203 | /** 204 | * @dataProvider esclines 205 | * @param string $input 206 | * @param string $remainder 207 | */ 208 | public function testEsclines(string $input, string $remainder): void 209 | { 210 | self::assertEquals($remainder, (string) escline()->tryString($input)->remainder()); 211 | } 212 | 213 | public function esclines(): array 214 | { 215 | return [ 216 | ["\\\nfoo", 'foo'], 217 | ["\\\n foo", ' foo'], 218 | ["\\ \t \nfoo", 'foo'], 219 | ["\\ // test \nfoo", 'foo'], 220 | ["\\ // test \n foo", ' foo'], 221 | ]; 222 | } 223 | 224 | /** 225 | * @dataProvider ws 226 | * @param string $input 227 | * @param string $remainder 228 | */ 229 | public function testWs(string $input, string $remainder): void 230 | { 231 | $this->makeRemainderAssertionsForParser($input, $remainder, ws()); 232 | } 233 | 234 | public function ws(): array 235 | { 236 | return [ 237 | [' ', ''], 238 | ["\t", ''], 239 | ["/* \nfoo\r\n */ etc", ' etc'], 240 | ["hi", self::ERROR], 241 | ]; 242 | } 243 | 244 | /** 245 | * @dataProvider newlines 246 | * @param string $input 247 | * @param string $remainder 248 | */ 249 | public function testNewlines(string $input, string $remainder): void 250 | { 251 | $this->makeRemainderAssertionsForParser($input, $remainder, newline()); 252 | } 253 | 254 | public function newlines(): array 255 | { 256 | return [ 257 | ["\n", ''], 258 | ["\r", ''], 259 | ["\r\n", ''], 260 | ["\n\n", "\n"], 261 | ['blah', self::ERROR], 262 | ]; 263 | } 264 | 265 | /** 266 | * @dataProvider nodesWithSlashdashComments 267 | * @param string $input 268 | * @param string $remainder 269 | */ 270 | public function testNodesWithSlashdashComments(string $input, string $remainder): void 271 | { 272 | $this->makeRemainderAssertionsForParser($input, $remainder, nodes()); 273 | } 274 | 275 | public function nodesWithSlashdashComments(): array 276 | { 277 | return [ 278 | ["/-node", ''], 279 | ["/- node", ''], 280 | ["/- node\n", ''], 281 | ["/-node 1 2 3", ''], 282 | ["/-node key=false", ''], 283 | ["/-node{\nnode\n}", ''], 284 | ["/-node 1 2 3 key=\"value\" \\\n{\nnode\n}", ''], 285 | ]; 286 | } 287 | 288 | /** 289 | * @dataProvider argSlashdashComments 290 | * @param string $input 291 | * @param array $values 292 | */ 293 | public function testArgSlashdashComments(string $input, array $values): void 294 | { 295 | /** @var Document $parsed */ 296 | $parsed = nodes()->thenEof()->tryString($input)->output(); 297 | self::assertInstanceOf(Document::class, $parsed); 298 | self::assertEquals($values, $parsed->getNodes()[0]->getValues()); 299 | } 300 | 301 | public function argSlashdashComments(): array 302 | { 303 | return [ 304 | ["node /-1", []], 305 | ["node /-1 2", [2]], 306 | ["node 1 /- 2 3", [1, 3]], 307 | ["node /--1", []], 308 | ["node /- -1", []], 309 | ["node \\\n/- -1", []], 310 | ]; 311 | } 312 | 313 | /** 314 | * @dataProvider propSlashdashComments 315 | * @param string $input 316 | * @param array $properties 317 | */ 318 | public function testPropSlashdashComments(string $input, array $properties): void 319 | { 320 | /** @var Document $parsed */ 321 | $parsed = nodes()->thenEof()->tryString($input)->output(); 322 | self::assertInstanceOf(Document::class, $parsed); 323 | self::assertEquals($properties, $parsed->getNodes()[0]->getProperties()); 324 | } 325 | 326 | public function propSlashdashComments(): array 327 | { 328 | return [ 329 | ["node /-key=1", []], 330 | ["node /- key=1", []], 331 | ["node key=1 /-key2=2", ['key' => 1]], 332 | ]; 333 | } 334 | 335 | /** 336 | * @dataProvider childrenSlashDashComments 337 | * @param string $input 338 | */ 339 | public function testChildrenSlashDashComments(string $input): void 340 | { 341 | /** @var Document $parsed */ 342 | $parsed = nodes()->thenEof()->tryString($input)->output(); 343 | self::assertInstanceOf(Document::class, $parsed); 344 | self::assertCount(0, $parsed->getNodes()[0]->getChildren()); 345 | } 346 | 347 | public function childrenSlashDashComments(): array 348 | { 349 | return [ 350 | ["node /-{}"], 351 | ["node /- {}"], 352 | ["node /-{\nnode2\n}"], 353 | ]; 354 | } 355 | 356 | private function makeRemainderAssertionsForParser(string $input, string $remainder, Parser $parser): void 357 | { 358 | if ($remainder === self::ERROR) { 359 | $this->assertParseFail($input, $parser); 360 | 361 | return; 362 | } 363 | self::assertEquals($remainder, (string) $parser->tryString($input)->remainder()); 364 | } 365 | 366 | private function makeAssertionsForParser(string $input, $output, Parser $parser): void 367 | { 368 | if ($output !== self::ERROR) { 369 | self::assertEquals($output, $parser->thenEof()->tryString($input)->output()); 370 | } else { 371 | $this->assertParseFail($input, $parser); 372 | } 373 | } 374 | 375 | private function assertParseFail(string $input, Parser $parser): void 376 | { 377 | try { 378 | $result = $parser->tryString($input); 379 | } catch (ParserHasFailed $e) { 380 | $this->expectNotToPerformAssertions(); 381 | return; 382 | } 383 | self::assertGreaterThan(0, strlen((string) $result->remainder())); 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /tests/NodeTest.php: -------------------------------------------------------------------------------- 1 | node = new Node( 21 | 'name', 22 | ['values'], 23 | ['properties' => true] 24 | ); 25 | } 26 | 27 | public function testIsNode(): void 28 | { 29 | self::assertInstanceOf(NodeInterface::class, $this->node); 30 | } 31 | 32 | public function testGetters(): void 33 | { 34 | self::assertEquals('name', $this->node->getName()); 35 | self::assertEquals(['values'], $this->node->getValues()); 36 | self::assertEquals(['properties' => true], $this->node->getProperties()); 37 | self::assertEquals([], $this->node->getChildren()); 38 | } 39 | 40 | public function testAttachChild(): void 41 | { 42 | $child = new Node( 43 | 'child', 44 | [], 45 | [] 46 | ); 47 | $this->node->attachChild($child); 48 | self::assertSame($child, $this->node->getChildren()[0]); 49 | } 50 | 51 | public function testSerialize(): void 52 | { 53 | $expectedSerialization = [ 54 | 'name' => 'name', 55 | 'values' => ['values'], 56 | 'properties' => (object)['properties' => true], 57 | 'children' => [ 58 | [ 59 | 'name' => 'child', 60 | 'values' => [], 61 | 'properties' => (object)[], 62 | 'children' => [], 63 | ], 64 | ], 65 | ]; 66 | $child = new Node( 67 | 'child', 68 | [], 69 | [] 70 | ); 71 | $this->node->attachChild($child); 72 | self::assertEquals($expectedSerialization, $this->node->jsonSerialize()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/ParserTest.php: -------------------------------------------------------------------------------- 1 | parser = new Parser(); 18 | } 19 | 20 | /** 21 | * @dataProvider kdlNodes 22 | * @param array $expectedNodeShape 23 | * @param string $kdl 24 | */ 25 | public function testNodes(array $expectedNodeShape, string $kdl): void 26 | { 27 | $result = $this->parser->parse($kdl); 28 | self::assertEquals($expectedNodeShape, $result->jsonSerialize()); 29 | } 30 | 31 | public function kdlNodes(): array 32 | { 33 | $suite = json_decode(file_get_contents(__DIR__ . '/suite.json') ?: '', true); 34 | $testData = []; 35 | foreach ($suite as $suiteName => $suiteData) { 36 | $testData[] = [$this->completeSuiteNodes($suiteData), $this->getKdlFile($suiteName)]; 37 | } 38 | 39 | return $testData; 40 | } 41 | 42 | public function testParseFail(): void 43 | { 44 | $badKdl = "node node"; 45 | $this->expectException(ParseException::class); 46 | $this->parser->parse($badKdl); 47 | } 48 | 49 | private function getKdlFile(string $name): string 50 | { 51 | return file_get_contents(sprintf('%s/kdl/%s.kdl', __DIR__, $name)) ?: ''; 52 | } 53 | 54 | private function completeSuiteNodes(array $nodeData): array 55 | { 56 | return array_map( 57 | function ($node) { 58 | return array_merge( 59 | [ 60 | 'values' => [], 61 | 'properties' => (object)[], 62 | ], 63 | array_merge( 64 | $node, 65 | [ 66 | 'children' => (array_key_exists('children', $node)) 67 | ? $this->completeSuiteNodes($node['children']) 68 | : [], 69 | 'properties' => (object) ($node['properties'] ?? []), 70 | ] 71 | ), 72 | ); 73 | }, 74 | $nodeData 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/bench/ParseBench.php: -------------------------------------------------------------------------------- 1 | parse($kdl); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/kdl/ci.kdl: -------------------------------------------------------------------------------- 1 | // This example is a GitHub Action if it used KDL syntax. 2 | // See .github/workflows/ci.yml for the file this was based on. 3 | name "CI" 4 | 5 | on "push" "pull_request" 6 | 7 | env { 8 | RUSTFLAGS "-Dwarnings" 9 | } 10 | 11 | jobs { 12 | fmt_and_docs "Check fmt & build docs" { 13 | runs-on "ubuntu-latest" 14 | steps { 15 | step uses="actions/checkout@v1" 16 | step "Install Rust" uses="actions-rs/toolchain@v1" { 17 | profile "minimal" 18 | toolchain "stable" 19 | components "rustfmt" 20 | override true 21 | } 22 | step "rustfmt" run="cargo fmt --all -- --check" 23 | step "docs" run="cargo doc --no-deps" 24 | } 25 | } 26 | build_and_test "Build & Test" { 27 | runs-on "${{ matrix.os }}" 28 | strategy { 29 | matrix { 30 | rust "1.46.0" "stable" 31 | os "ubuntu-latest" "macOS-latest" "windows-latest" 32 | } 33 | } 34 | 35 | steps { 36 | step uses="actions/checkout@v1" 37 | step "Install Rust" uses="actions-rs/toolchain@v1" { 38 | profile "minimal" 39 | toolchain "${{ matrix.rust }}" 40 | components "clippy" 41 | override true 42 | } 43 | step "Clippy" run="cargo clippy --all -- -D warnings" 44 | step "Run tests" run="cargo test --all --verbose" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/kdl/nodes_1.kdl: -------------------------------------------------------------------------------- 1 | node -------------------------------------------------------------------------------- /tests/kdl/nodes_2.kdl: -------------------------------------------------------------------------------- 1 | node 2 | -------------------------------------------------------------------------------- /tests/kdl/nodes_3.kdl: -------------------------------------------------------------------------------- 1 | 2 | node 3 | -------------------------------------------------------------------------------- /tests/kdl/nodes_4.kdl: -------------------------------------------------------------------------------- 1 | node1 2 | node2 -------------------------------------------------------------------------------- /tests/kdl/nodes_5.kdl: -------------------------------------------------------------------------------- 1 | node; -------------------------------------------------------------------------------- /tests/kdl/nodes_6.kdl: -------------------------------------------------------------------------------- 1 | node 1 2 "3" true false null -------------------------------------------------------------------------------- /tests/kdl/nodes_7.kdl: -------------------------------------------------------------------------------- 1 | node { 2 | node2 3 | } -------------------------------------------------------------------------------- /tests/kdl/nodes_8.kdl: -------------------------------------------------------------------------------- 1 | node { node2; } -------------------------------------------------------------------------------- /tests/kdl/nodes_9.kdl: -------------------------------------------------------------------------------- 1 | node key=1 apple="green" key=2 -------------------------------------------------------------------------------- /tests/kdl/value_number_decimal.kdl: -------------------------------------------------------------------------------- 1 | node 123456789 2 | node +123456789 3 | node -123456789 4 | node 0123456789 5 | node +0123456789 6 | node -0123456789 7 | 8 | node 12345678E9 9 | 10 | node 12345678e9 11 | node -12345678e9 12 | node 12345678e-9 13 | node -12345678e-9 14 | 15 | node 1234.56789 16 | node +1234.56789 17 | node -1234.56789 18 | 19 | node 1234.5678e9 20 | node -1234.5678e9 21 | node 1234.5678e-9 22 | node -1234.5678e-9 23 | -------------------------------------------------------------------------------- /tests/kdl/value_number_other.kdl: -------------------------------------------------------------------------------- 1 | node 0b10 2 | node 0b1100100 3 | 4 | node 0o10 5 | node 0o144 6 | 7 | node 0x10 8 | node 0x64 9 | -------------------------------------------------------------------------------- /tests/kdl/value_number_separator.kdl: -------------------------------------------------------------------------------- 1 | node 0_1__2___3 2 | node 0b0_1__10___11 3 | node 0o0_1__2___3 4 | node 0x0_1__2___3 5 | 6 | node 1_1_2__3___5_____8________13_____________21 7 | -------------------------------------------------------------------------------- /tests/kdl/value_other.kdl: -------------------------------------------------------------------------------- 1 | node true 2 | node false 3 | node null 4 | -------------------------------------------------------------------------------- /tests/kdl/website.kdl: -------------------------------------------------------------------------------- 1 | doctype "html" 2 | html lang="en" { 3 | head { 4 | meta charset="utf-8" 5 | meta name="viewport" content="width=device-width, initial-scale=1.0" 6 | meta \ 7 | name="description" \ 8 | content="kdl is a document language, mostly based on SDLang, with xml-like semantics that looks like you're invoking a bunch of CLI commands!" 9 | title "kdl - Kat's Document Language" 10 | link rel="stylesheet" href="/styles/global.css" 11 | } 12 | body { 13 | main { 14 | header class="py-10 bg-gray-300" { 15 | h1 class="text-4xl text-center" "kdl - Kat's Document Language" 16 | } 17 | section class="kdl-section" id="description" { 18 | p { 19 | text "kdl is a document language, mostly based on " 20 | a href="https://sdlang.org" "SDLang" 21 | text " with xml-like semantics that looks like you're invoking a bunch of CLI commands" 22 | } 23 | p "It's meant to be used both as a serialization format and a configuration language, and is relatively light on syntax compared to XML." 24 | } 25 | section class="kdl-section" id="design-and-discussion" { 26 | h2 "Design and Discussion" 27 | p { 28 | text "kdl is still extremely new, and discussion about the format should happen over on the " 29 | a href="https://github.com/kdoclang/kdl/discussions" { 30 | text "discussions" 31 | } 32 | text " page in the Github repo. Feel free to jump in and give us your 2 cents!" 33 | } 34 | } 35 | section class="kdl-section" id="design-principles" { 36 | h2 "Design Principles" 37 | ol { 38 | li "Maintainability" 39 | li "Flexibility" 40 | li "Cognitive simplicity and Learnability" 41 | li "Ease of de/serialization" 42 | li "Ease of implementation" 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/suite.json: -------------------------------------------------------------------------------- 1 | { 2 | "value_number_decimal": [ 3 | { "name": "node", "values": [123456789] }, 4 | { "name": "node", "values": [123456789] }, 5 | { "name": "node", "values": [-123456789] }, 6 | { "name": "node", "values": [123456789] }, 7 | { "name": "node", "values": [123456789] }, 8 | { "name": "node", "values": [-123456789] }, 9 | { "name": "node", "values": [12345678000000000] }, 10 | { "name": "node", "values": [12345678000000000] }, 11 | { "name": "node", "values": [-12345678000000000] }, 12 | { "name": "node", "values": [0.012345678] }, 13 | { "name": "node", "values": [-0.012345678] }, 14 | { "name": "node", "values": [1234.56789] }, 15 | { "name": "node", "values": [1234.56789] }, 16 | { "name": "node", "values": [-1234.56789] }, 17 | { "name": "node", "values": [1234567800000] }, 18 | { "name": "node", "values": [-1234567800000] }, 19 | { "name": "node", "values": [0.0000012345678] }, 20 | { "name": "node", "values": [-0.0000012345678] } 21 | ], 22 | "value_number_other": [ 23 | { "name": "node", "values": [2] }, 24 | { "name": "node", "values": [100] }, 25 | { "name": "node", "values": [8] }, 26 | { "name": "node", "values": [100] }, 27 | { "name": "node", "values": [16] }, 28 | { "name": "node", "values": [100] } 29 | ], 30 | "value_number_separator": [ 31 | { "name": "node", "values": [123] }, 32 | { "name": "node", "values": [27] }, 33 | { "name": "node", "values": [83] }, 34 | { "name": "node", "values": [291] }, 35 | { "name": "node", "values": [1123581321] } 36 | ], 37 | "value_other": [ 38 | { "name": "node", "values": [true] }, 39 | { "name": "node", "values": [false] }, 40 | { "name": "node", "values": [null] } 41 | ], 42 | "nodes_1": [ 43 | { "name": "node" } 44 | ], 45 | "nodes_2": [ 46 | { "name": "node" } 47 | ], 48 | "nodes_3": [ 49 | { "name": "node" } 50 | ], 51 | "nodes_4": [ 52 | { "name": "node1" }, 53 | { "name": "node2" } 54 | ], 55 | "nodes_5": [ 56 | { "name": "node" } 57 | ], 58 | "nodes_6": [ 59 | { "name": "node", "values": [1, 2, "3", true, false, null] } 60 | ], 61 | "nodes_7": [ 62 | { "name": "node", "children": [ { "name": "node2" } ] } 63 | ], 64 | "nodes_8": [ 65 | { "name": "node", "children": [ { "name": "node2" } ] } 66 | ], 67 | "nodes_9": [ 68 | { "name": "node", "properties": { "key": 2, "apple": "green" } } 69 | ], 70 | "website": [ 71 | { "name": "doctype", "values": ["html"] }, 72 | { 73 | "name": "html", 74 | "properties": { "lang": "en" }, 75 | "children": [ 76 | { 77 | "name": "head", 78 | "children": [ 79 | { 80 | "name": "meta", 81 | "properties": { "charset": "utf-8" } 82 | }, 83 | { 84 | "name": "meta", 85 | "properties": { 86 | "name": "viewport", 87 | "content": "width=device-width, initial-scale=1.0" 88 | } 89 | }, 90 | { 91 | "name": "meta", 92 | "properties": { 93 | "name": "description", 94 | "content": "kdl is a document language, mostly based on SDLang, with xml-like semantics that looks like you're invoking a bunch of CLI commands!" 95 | } 96 | }, 97 | { 98 | "name": "title", 99 | "values": ["kdl - Kat's Document Language"] 100 | }, 101 | { 102 | "name": "link", 103 | "properties": { 104 | "rel": "stylesheet", 105 | "href": "/styles/global.css" 106 | } 107 | } 108 | ] 109 | }, 110 | { 111 | "name": "body", 112 | "children": [ 113 | { 114 | "name": "main", 115 | "children": [ 116 | { 117 | "name": "header", 118 | "properties": { "class": "py-10 bg-gray-300" }, 119 | "children": [ 120 | { 121 | "name": "h1", 122 | "properties": { "class": "text-4xl text-center" }, 123 | "values": ["kdl - Kat's Document Language"] 124 | } 125 | ] 126 | }, 127 | { 128 | "name": "section", 129 | "properties": { 130 | "class": "kdl-section", 131 | "id": "description" 132 | }, 133 | "children": [ 134 | { 135 | "name": "p", 136 | "children": [ 137 | { 138 | "name": "text", 139 | "values": ["kdl is a document language, mostly based on "] 140 | }, 141 | { 142 | "name": "a", 143 | "properties": { "href": "https://sdlang.org" }, 144 | "values": ["SDLang"] 145 | }, 146 | { 147 | "name": "text", 148 | "values": [" with xml-like semantics that looks like you're invoking a bunch of CLI commands"] 149 | } 150 | ] 151 | }, 152 | { 153 | "name": "p", 154 | "values": ["It's meant to be used both as a serialization format and a configuration language, and is relatively light on syntax compared to XML."] 155 | } 156 | ] 157 | }, 158 | { 159 | "name": "section", 160 | "properties": { 161 | "class": "kdl-section", 162 | "id": "design-and-discussion" 163 | }, 164 | "children": [ 165 | { 166 | "name": "h2", 167 | "values": ["Design and Discussion"] 168 | }, 169 | { 170 | "name": "p", 171 | "children": [ 172 | { 173 | "name": "text", 174 | "values": ["kdl is still extremely new, and discussion about the format should happen over on the "] 175 | }, 176 | { 177 | "name": "a", 178 | "properties": { "href": "https://github.com/kdoclang/kdl/discussions" }, 179 | "children": [ 180 | { 181 | "name": "text", 182 | "values": ["discussions"] 183 | } 184 | ] 185 | }, 186 | { 187 | "name": "text", 188 | "values": [" page in the Github repo. Feel free to jump in and give us your 2 cents!"] 189 | } 190 | ] 191 | } 192 | ] 193 | }, 194 | { 195 | "name": "section", 196 | "properties": { 197 | "class": "kdl-section", 198 | "id": "design-principles" 199 | }, 200 | "children": [ 201 | { 202 | "name": "h2", 203 | "values": ["Design Principles"] 204 | }, 205 | { 206 | "name": "ol", 207 | "children": [ 208 | { 209 | "name": "li", 210 | "values": ["Maintainability"] 211 | }, 212 | { 213 | "name": "li", 214 | "values": ["Flexibility"] 215 | }, 216 | { 217 | "name": "li", 218 | "values": ["Cognitive simplicity and Learnability"] 219 | }, 220 | { 221 | "name": "li", 222 | "values": ["Ease of de/serialization"] 223 | }, 224 | { 225 | "name": "li", 226 | "values": ["Ease of implementation"] 227 | } 228 | ] 229 | } 230 | ] 231 | } 232 | ] 233 | } 234 | ] 235 | } 236 | ] 237 | } 238 | ], 239 | "ci": [ 240 | { 241 | "name": "name", 242 | "values": ["CI"] 243 | }, 244 | { 245 | "name": "on", 246 | "values": ["push", "pull_request"] 247 | }, 248 | { 249 | "name": "env", 250 | "children": [ 251 | { 252 | "name": "RUSTFLAGS", 253 | "values": ["-Dwarnings"] 254 | } 255 | ] 256 | }, 257 | { 258 | "name": "jobs", 259 | "children": [ 260 | { 261 | "name": "fmt_and_docs", 262 | "values": ["Check fmt & build docs"], 263 | "children": [ 264 | { 265 | "name": "runs-on", 266 | "values": ["ubuntu-latest"] 267 | }, 268 | { 269 | "name": "steps", 270 | "children": [ 271 | { 272 | "name": "step", 273 | "properties": { "uses": "actions/checkout@v1" } 274 | }, 275 | { 276 | "name": "step", 277 | "values": ["Install Rust"], 278 | "properties": { "uses": "actions-rs/toolchain@v1" }, 279 | "children": [ 280 | { 281 | "name": "profile", 282 | "values": ["minimal"] 283 | }, 284 | { 285 | "name": "toolchain", 286 | "values": ["stable"] 287 | }, 288 | { 289 | "name": "components", 290 | "values": ["rustfmt"] 291 | }, 292 | { 293 | "name": "override", 294 | "values": [true] 295 | } 296 | ] 297 | }, 298 | { 299 | "name": "step", 300 | "values": ["rustfmt"], 301 | "properties": { "run": "cargo fmt --all -- --check" } 302 | }, 303 | { 304 | "name": "step", 305 | "values": ["docs"], 306 | "properties": { "run": "cargo doc --no-deps" } 307 | } 308 | ] 309 | } 310 | ] 311 | }, 312 | { 313 | "name": "build_and_test", 314 | "values": ["Build & Test"], 315 | "children": [ 316 | { 317 | "name": "runs-on", 318 | "values": ["${{ matrix.os }}"] 319 | }, 320 | { 321 | "name": "strategy", 322 | "children": [ 323 | { 324 | "name": "matrix", 325 | "children": [ 326 | { 327 | "name": "rust", 328 | "values": ["1.46.0", "stable"] 329 | }, 330 | { 331 | "name": "os", 332 | "values": ["ubuntu-latest", "macOS-latest", "windows-latest"] 333 | } 334 | ] 335 | } 336 | ] 337 | }, 338 | { 339 | "name": "steps", 340 | "children": [ 341 | { 342 | "name": "step", 343 | "properties": { "uses": "actions/checkout@v1" } 344 | }, 345 | { 346 | "name": "step", 347 | "values": ["Install Rust"], 348 | "properties": { "uses": "actions-rs/toolchain@v1" }, 349 | "children": [ 350 | { 351 | "name": "profile", 352 | "values": ["minimal"] 353 | }, 354 | { 355 | "name": "toolchain", 356 | "values": ["${{ matrix.rust }}"] 357 | }, 358 | { 359 | "name": "components", 360 | "values": ["clippy"] 361 | }, 362 | { 363 | "name": "override", 364 | "values": [true] 365 | } 366 | ] 367 | }, 368 | { 369 | "name": "step", 370 | "values": ["Clippy"], 371 | "properties": { "run": "cargo clippy --all -- -D warnings" } 372 | }, 373 | { 374 | "name": "step", 375 | "values": ["Run tests"], 376 | "properties": { "run": "cargo test --all --verbose" } 377 | } 378 | ] 379 | } 380 | ] 381 | } 382 | ] 383 | } 384 | ] 385 | } 386 | --------------------------------------------------------------------------------