Editing NEON Files

A configuration file is written by people: comments, blank lines, tabs or spaces, a hexadecimal number, yes instead of true. A tool that changes one option and writes the file back has to keep all of that. This page shows how to:

  • read a file into a syntax tree that keeps every character of it
  • change, add, remove and rename items, so that the diff is only your change
  • find items and comments in the tree

The libraries for PHP, JavaScript and Python edit files the same way, as the specification describes. Choose your language above any example and all examples on the page switch to it. Python writes the names in snake_case, so setValue() is set_value() there.

Reading a File into the Tree

The examples edit this file config.neon:

# database connection
database:
	host: localhost   # the server
	port: 0x0CEA
	# for development only
	debug: yes

services:
	- App\Model\ArticleFacade

mail:
	smtp: true

First, parse it into a syntax tree. Unlike decoding into values, the tree keeps every character of the file, so printing it gives the file back byte for byte:

use Nette\Neon\Neon;

$text = file_get_contents('config.neon');
$document = Neon::parse($text);
$document->print() === $text; // true
import { readFileSync } from 'node:fs';
import { parse } from '@nette/neon';

const text = readFileSync('config.neon', 'utf8');
const doc = parse(text);
doc.print() === text; // true
from pathlib import Path
from neon import parse

text = Path("config.neon").read_text(encoding="utf-8")
doc = parse(text)
doc.print() == text  # True

The edits below change the tree, and you save the result by writing print() back to the file.

Changing Values

An item is found by its path, the list of keys from the root, the same keys that decoding gives:

$document->setValue(['database', 'host'], 'db.example.com');
$document->setValue(['database', 'port'], 3307);
$document->setValue(['database', 'debug'], false);
$document->setValue(['mail', 'smtp'], true);
doc.setValue(['database', 'host'], 'db.example.com');
doc.setValue(['database', 'port'], 3307);
doc.setValue(['database', 'debug'], false);
doc.setValue(['mail', 'smtp'], true);
doc.set_value(["database", "host"], "db.example.com")
doc.set_value(["database", "port"], 3307)
doc.set_value(["database", "debug"], False)
doc.set_value(["mail", "smtp"], True)

The file then differs in three lines:

- 	host: localhost   # the server
- 	port: 0x0CEA
- 	debug: yes
+ 	host: db.example.com   # the server
+ 	port: 0x0CEB
+ 	debug: no

Only the value changes; the spaces and the comment after it stay. The new value is written in the style of the old one: the port stays hexadecimal, and yes becomes no, not false.

The last call writes true where true already is, and the file does not change at all: writing an equal value changes nothing. A form that saves all its fields therefore does not rewrite the options nobody touched.

Adding Items

When the last key of the path does not exist, setValue() adds the item. The method addItem() adds an item without a key, an item of a list:

$document->setValue(['database', 'user'], 'admin');
$document->addItem(['services'], 'App\Model\OrderFacade');
doc.setValue(['database', 'user'], 'admin');
doc.addItem(['services'], 'App\\Model\\OrderFacade');
doc.set_value(["database", "user"], "admin")
doc.add_item(["services"], r"App\Model\OrderFacade")
  	debug: yes
+ 	user: admin

  	- App\Model\ArticleFacade
+ 	- App\Model\OrderFacade

A new item goes to the end of its parent, indented like its neighbours.

A key in the middle of the path that does not exist is an error, so that a typo does not create a new section. With the option create set to false, a missing last key is an error too:

$document->setValue(['databse', 'user'], 'admin');
// Nette\Neon\Exception: No item 'databse' in the path databse.user

$document->setValue(['database', 'pasword'], 'secret', create: false);
// Nette\Neon\Exception: No item 'pasword' in the path database.pasword, and create is false
doc.setValue(['databse', 'user'], 'admin');
// NeonError: No item 'databse' in the path databse.user

doc.setValue(['database', 'pasword'], 'secret', { create: false });
// NeonError: No item 'pasword' in the path database.pasword, and create is false
doc.set_value(["databse", "user"], "admin")
# NeonError: No item 'databse' in the path databse.user

doc.set_value(["database", "pasword"], "secret", create=False)
# NeonError: No item 'pasword' in the path database.pasword, and create is false

A refused edit changes nothing in the tree, not even half of itself.

Removing Items

$document->removeItem(['database', 'debug']);
$document->removeItem(['mail', 'smtp']);
doc.removeItem(['database', 'debug']);
doc.removeItem(['mail', 'smtp']);
doc.remove_item(["database", "debug"])
doc.remove_item(["mail", "smtp"])
  	port: 0x0CEA
- 	# for development only
- 	debug: yes

- mail:
- 	smtp: true
+ mail: {}

An item takes with it the comment on its line and the comments directly above it, because they describe it. A comment separated from the item by a blank line is a heading of what follows, and it stays.

Removing the last item keeps the collection: mail becomes {}, because in a configuration an empty section and no section at all often mean different things. Where they mean the same, pass the option nullable, and the key is left without a value:

$document->removeItem(['mail', 'smtp'], nullable: true);
// mail:
doc.removeItem(['mail', 'smtp'], { nullable: true });
// mail:
doc.remove_item(["mail", "smtp"], nullable=True)
# mail:

The comment # database connection stands directly above database, so removing database removes it as well. When you want to keep such a comment, ask for it to be moved to whatever follows:

use Nette\Neon\CommentPolicy;

$document->removeItem(['database'], CommentPolicy::MoveToNextToken);
import { CommentPolicy } from '@nette/neon';

doc.removeItem(['database'], { comments: CommentPolicy.MoveToNextToken });
from neon import CommentPolicy

doc.remove_item(["database"], comments=CommentPolicy.MOVE_TO_NEXT_TOKEN)

Renaming Keys

The method setKey() renames a key and leaves its value as it is:

$document->setKey(['mail'], 'mailer');
doc.setKey(['mail'], 'mailer');
doc.set_key(["mail"], "mailer")

The Tree

The tree is made of nodes, and the nodes of tokens, each with its exact text and with its trivia: the whitespace, line endings and comments around it. The method findItem() finds an item by its path:

$item = $document->findItem(['database', 'host']);
$item->getValue();                       // 'localhost'
$item->getTrailingComments()[0]->text;   // '# the server'
$item->value->getFirstToken()->text;     // 'localhost', the text exactly as written
$item->getPosition();                    // line 3, column 2
const item = doc.findItem(['database', 'host']);
item.getValue();                         // 'localhost'
item.getTrailingComments()[0].text;      // '# the server'
item.value.getFirstToken().text;         // 'localhost', the text exactly as written
item.getPosition();                      // line 3, column 2
item = doc.find_item(["database", "host"])
item.get_value()                         # 'localhost'
item.get_trailing_comments()[0].text     # '# the server'
item.value.get_first_token().text        # 'localhost', the text exactly as written
item.get_position()                      # line 3, column 2

There are eight classes of nodes: DocumentNode, BlockArrayNode, InlineArrayNode, ArrayItemNode, EntityNode, EntityChainNode, StringNode and LiteralNode. A node finds its descendants of a given class with find(), its ancestor with findAncestor(), and the class Traverser walks the tree and can replace or remove nodes on the way. A position is always computed from the current tree, so it stays right after edits.

Each library has runnable examples that go further, for example a form over a configuration file: PHP, JavaScript, Python.