├── .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 | [](//packagist.org/packages/kdl/kdl) [](//packagist.org/packages/kdl/kdl) 
2 |
3 | # KDL-PHP
4 |
5 | A PHP library for the [KDL Document Language](https://kdl.dev) (pronounced like "cuddle").
6 |
7 | 
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 |
--------------------------------------------------------------------------------