Skip to content

6. Writing

Contents of FBL 0.6

6.1 The splice catalogue

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).

6.2 Inserting and removing

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), or (FBL 0.4) end (after the last member of the container's parent, which for a member of the root is the end of the document); 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. (FBL 0.4) With end, the container's parent may be missing too: see Missing levels below.
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. (FBL 0.6) An attribute the add does not give has no value, and its key is left out whatever its binding's empty says. An attribute the add gives its empty value (section 5.2) has a value, and a host MUST write or leave out its key by that attribute binding's empty, with the defaults a set has: keep writes the key with the empty value, in the form section 6.3 gives it (in yaml the empty string is not plain-safe, so it is quoted, key: "" with the default text.quote; in json "key": ""); remove leaves the key out; refuse refuses the add (section 6.4). A binding without empty MUST give the key what a set of that attribute to its empty value gives the slot, which for a string attribute is keep. So one binding gives the same entry for an add as for an add followed by a set to the same value. A new entry written with emit follows emit's own rule for optional segments instead.
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. (FBL 0.3) A container that encloses its entries on one line (a json array or object written on one line) cannot be written before an entry that goes inside it, so it is created as two ensure-container splices, its opening and its closing, with the insert-entry between them, all three at the same offset; removing such a container with its last entry is likewise its opening (remove-container), the entry (remove-entry) and its closing (remove-container), as self-close splits an xml element around its children.

Missing levels (FBL 0.4). An insert whose create.at is end may find more than its container missing: container: "/view/placements" in a body without view. Each missing level is created, outermost first, at the end of the level above it, and the new entry is inserted into the innermost; all of these splices are at one offset. Every level but the container itself is written as a mapping. In yaml a level is the key alone with an empty block value, one indentation step deeper than the level above it (section 6.3), and is one ensure-container splice; the levels' splices come first, outermost first, then the entry's. In json a level is "key": { and its closing }, and in xml an element with the key's name; each such level is two ensure-container splices, its opening and its closing, each on its own line, so that the openings come outermost first, then the entry, then the closings innermost first, as a container written on one line is created above. Only levels named by key segments are created: when a level a * or {capture} segment names is missing, there is no key to write, and the insert is refused. The same holds for an attribute's create (section 5.2), whose levels are those of its child selector. Removing the entry undoes this only with remove-empty-levels: a level is removed when nothing is left in it, which with remove-when-empty happens to the container alone.

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. (FBL 0.4) remove-empty-levels: it is removed, and so is each level of its selector above it that is left empty with it, up to the first level that still holds something, a level a * or {capture} segment names, or the root; each level is removed by remove-container splices in the order Missing levels below creates it, around the entry's own.

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.

6.3 New text

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 []. (FBL 0.5) A value written for a slot read as text MUST read back through the same binding as the same text. The plain-safe test above still holds for it, so the text yes or 1.0 is written quoted, and a file another tool reads keeps its meaning there; an attribute whose style is plain is written plain whenever YAML's syntax allows it, for a format whose every reader takes the written text.

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.

6.4 Edits

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 model change of section 11.2 ((FBL 0.2) an add, set, remove, move or retype, with the fields given there) 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" on a set or, (FBL 0.6), on an add that gives the attribute its empty value (section 6.2), 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.

(FBL 0.2) A declared binding plans the changes of section 11.2 as a plugin that declares add.position and move does. An add with a position is one insert-entry directly before the entry of the sibling at that index (or after the last sibling's entry when the index equals their number), in place of the rule's insert.place; a rule whose insert cannot place it there refuses the add. A move is the remove-entry and insert-entry of section 5.5, the inserted text being the entry's own bytes, at its position. A retype is planned as a remove and an add of the element under its new type's rule, and the host warns as section 11.2 says.

6.5 Determinism

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.

6.6 Saving

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.

Referenced from