6. Writing
Contents of FBL 0.1
Every write is a splice: the replacement of one byte range of the body (possibly empty) with new text (possibly empty). Every splice is one of the eleven operations below, and every edit consists of such splices only. Each has an exact inverse: the splice that puts the replaced bytes back.
| Operation | Replaces | With | Families |
|---|---|---|---|
replace-value |
a value's span, quotes included | the new value, in the value's own style when it fits (section 6.3) | all |
insert-key |
nothing, at the position insert.keys or the attribute order gives |
a new key and value (yaml, json), attribute (xml), optional segment or word (lines, blocks), or child element holding a value (xml create) |
all |
remove-key |
a key or attribute and the trivia it owns; a word and the whitespace before it; an override slot; a created child |
nothing | all |
insert-entry |
nothing, at the place insert.place gives |
a new entry with its separators and line endings | all |
remove-entry |
an entry's line span, or its own span and one separator | nothing | all |
ensure-container |
nothing, at the place insert.create gives |
a missing container (a key with an empty block value, an array, an element) | yaml, json, xml; the registration's blocks |
remove-container |
a container left empty and its line span | nothing | yaml, json, xml; the registration's blocks |
rewrite-reference |
a reference's span | the new value it names | all |
re-emit-line |
a statement's own span | the statement emitted from its rule's emit with the element's current values |
lines, blocks |
open-block |
nothing, before the statement's line ending; and nothing, after the block's last line | {; and a closing } line indented like the statement |
blocks |
self-close |
the > of a start tag and the end tag with the whitespace before it; or /> |
/>; or > and a new end tag |
xml |
A host MUST NOT write a body by any other means than these splices, except when the user explicitly replaces the body with the editor's own version after an external change (section 7.3).
Insert. A rule's insert says where a new entry goes:
| Property | Description |
|---|---|
place |
Required. after-last: right after the last entry of this rule in the container (document order); if there is none, at end. end: after the container's last entry. start: before its first entry. last-child: after the parent entry's last child (xml, blocks), opening the parent first when it is self-closed (self-close) or one-line (open-block). next-sibling: right after the entry the gesture was made on. end-of-document: after the last byte of the body. {before: key}: before the member key of the container. |
container |
Selector of the container (tree, xml), or the name of the enclosing block rule (blocks). Relative selectors resolve against the parent entry. {capture} segments resolve to the values the registration or the parent entry bound them to. |
create |
When the container does not exist: at is end-of-document, {before: key} (before a member of the container's parent), {after: key} (right after a key of the entry, as the first key after it), or {under: selector} (at the end of that container); text, when given, is the container's own text, otherwise the key alone with an empty block value (yaml), "key": [] (json) or an empty element (xml). One ensure-container splice. |
keys |
The order of keys in a new entry (tree) or of its attributes (xml). A key the new entry has no value for is left out. |
emit |
The text of a new entry for families without keys (lines, blocks, xml), and for json items written on one line: literal text with placeholders {attribute} (the attribute's written form, section 6.3), {id}, {source}, {target}; [ … ] encloses an optional segment, written only when every placeholder in it is non-empty. A flag attribute's placeholder writes the flag's word when true and nothing when false. |
skeleton |
Fixed text written with the entry after its keys, indented as its keys (a Databricks task's notebook_task). |
when |
An expression over attributes; when false, adding is refused. |
An insert is one insert-entry splice, preceded by an ensure-container splice at the same offset when the container is created. Splices at the same offset are applied in the order the edit lists them.
Remove. A rule's remove says what goes with an entry:
| Property | Description |
|---|---|
cascade |
Rules whose entries go too when they reference the removed entry (relations whose source or target is it, elements whose reference names it). Entries nested inside the removed entry's span go with it without being named. |
container |
keep (default): a container left empty stays. remove-when-empty: it is removed with a remove-container splice. |
A removal removes the entry's line span when the entry starts and ends its lines, else its own span. In json, removing an item also removes one separator: the one after it, or, for the last item, the one before it together with the whitespace between that separator and the item. Cascaded entries are removed bottom-up (last in document order first); all splices are one edit.
New text follows the body's own conventions where the body shows them, and the binding's text defaults only where it does not. A host MUST apply these rules exactly; they are what makes two hosts write the same bytes.
Line endings. A splice that writes line breaks uses the ending of the line its insertion point is on, or, when that line has none (the last line without a final newline), the body's dominant ending (the one that ends the most lines; CRLF when CRLF and LF tie; a lone CR counts as neither). A body without any line ending uses text.newline, default LF. An insertion after a last line that has no ending writes the line break before the new text and none after it, so the body still has no final newline.
Indentation. A new entry is indented like its previous sibling in the container, or, when it has none, like its next sibling, or, when it has neither, one indentation step deeper than its parent entry. The step is the difference between the parent's and a child's indentation anywhere in the body, the first such pair in document order; else text.indent (default 2, or a tab). A new yaml sequence item under a key follows text.sequenceIndent when the body shows no example: indented (default) puts - one step deeper than the key, flush in the key's column. Keys of a new mapping item align with the column after - .
yaml scalars. A replaced value keeps its style (plain, single, double, literal, folded) when the new value can be written in it; else, and for a new value, the style is chosen in this order: the attribute's style; plain if plain-safe; else text.quote (default double). A string is plain-safe when it is not empty; has no leading or trailing whitespace; contains no line break or control character; does not start with any of - ? : , [ ] { } # & * ! | > ' " % @ ` ; contains neither : nor # and does not end with :; and would be read back as the same string under the YAML 1.2 core schema and as a string under YAML 1.1 (so not null, ~, true, false, yes, no, on, off, y, n in any case, a number, or a date or date-time), unless the attribute's DISL type is the type it would read as. Double-quoted strings escape \, " and control characters (\n, \t, \r, \uXXXX); single-quoted strings double '. A multi-line string with style literal is written |- with its lines indented one step deeper than its key. A list with style flow is written [a, b], its items by the same rules; an empty list is [].
json values. Strings are written with the escapes RFC 8785 uses; numbers as below; true, false, null as such. A new member of an object or item of an array that is written one per line starts on a new line indented like its sibling; one in a container written on one line (or given by emit) is written after , on the same line. The separator of the previous last entry is written as part of the new entry's splice.
Numbers. With number: "shortest" (the default), an integer value is written without a fraction, and any other number as the shortest decimal representation that reads back as the same IEEE 754 double, in ECMAScript's Number.prototype.toString form (RFC 8785 §3.2.2.3). With {decimals: n}, the number is rounded to n decimals, halves away from zero, and trailing zeros and a trailing point are dropped.
Times. With time: "keep-precision", a date or date-time replacing a value is written with the precision of the value it replaces: a date as YYYY-MM-DD; a date-time as YYYY-MM-DDTHH:MM:SS, and with a fraction or an offset only if the replaced value had one. A new value is written in the precision the gesture gave it.
xml. New attributes are written in insert.keys order, then in the order of their binding; an attribute added to an existing element goes after its last attribute, preceded by one space. A new element goes on its own line when its previous sibling is on its own line, with that sibling's indentation. A self-closed tag is written /> without a space unless the element being replaced wrote one.
lines and blocks. A new statement is its rule's emit on its own line, indented (in blocks) one step deeper than its enclosing block's statement. A changed value is written by replace-value on its group's span. A value whose slot has no span in the statement (an absent optional group or word) is written by insert-key when the rule's emit places it after a present value and before nothing that must move, and otherwise by re-emit-line, which rewrites the statement in the canonical form its emit gives; unbound words of a re-emitted statement are lost, so a host MUST prefer insert-key when both are possible.
A user gesture, form commit, operation or quick fix is one DISL transaction (DISL §14.4). When the transaction commits, its model changes are planned as splices: each attribute set, element or relation added, removed or moved becomes the splices this section gives, in the order the transaction made them, then adjusted so that all offsets refer to the body before the edit. The result is one edit: its splices applied together, recorded together, undone together. If any change cannot be planned (no insert, no remove, a read-only slot, empty: "refuse", an absent refusal, a duplicate on rename, a plugin's refusal), the whole transaction is rejected with that reason and rolled back, and nothing is written.
For the same body, binding and edit, a conforming host MUST plan the same splices and write the same bytes. Offsets are UTF-8 byte offsets; splices of one edit do not overlap; splices at the same offset apply in the order listed.
A host writes a body when the user saves, or after every edit if the host saves automatically; either way what is written is the body's bytes after its edits. A body MUST be written atomically (a temporary file in the same folder, then moved into place), keeping the file's permissions. A body that is unreadable (section 7.5) or read-only is never written.