6. Writing
Contents of FBL 0.8
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, bullets), item's continuation (bullets), 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; an item's continuation |
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, a section's heading) | yaml, json, xml, bullets; 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, or an item's text | the statement or text emitted from its rule's emit with the element's current values |
lines, blocks, bullets |
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 |
(FBL 0.7) In a declared block of the registration whose order is kept, a move of an entry is a remove-entry and an insert-entry of the same bytes (section 8.4).
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, bullets), 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. (FBL 0.7) after-header: as a line of its own right after the line the binding's header matched (lines, blocks, bullets), as a mark line beside the header needs when it is missing. {before: key}: before the member key of the container. |
container |
Selector of the container (tree, xml), the name of the enclosing block rule (blocks), or (FBL 0.7) the name of the section (bullets); in bullets, the container of an add with a parent is that parent item, whose nested items are its entries. 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. (FBL 0.7) In bullets the container is a section: at is end-of-document, and text is its heading line, such as ## Behavior (section 6.3). |
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. (FBL 0.8) A constant's key that keys names takes that place, with the constant's value. Every other constant follows the entry's other keys, in the order constant lists its members, and comes before skeleton. |
emit |
The text of a new entry for families without keys (lines, blocks, xml, bullets), 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, and (FBL 0.7) \[ and \] write a literal bracket (\[ \] {text} writes an open task of a Markdown task list). A flag attribute's placeholder writes the flag's word when true and nothing when false. (FBL 0.7) The placeholder of an attribute with map writes the wire value that section 5.2 gives the value, and the placeholder of an attribute the add does not give writes its binding's default, or nothing without one. |
constant |
(FBL 0.8) yaml, json: a map from a key of the new entry to a scalar (a string, a number, a bool or null) that every new entry of the rule is written with, and that no attribute supplies, such as {"type": "ui-element"} for a rule chosen by when: "entry.type == 'ui-element'". Its place among the keys is in keys, above. MUST NOT be given with emit, nor in a binding of another family (section 14.1). |
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. |
Constants (FBL 0.8). A host MUST write every member of an insert's constant into each new entry of the rule, in the entry's one insert-entry splice, at the place keys gives it. Its value is written as section 6.3 writes a new value of its JSON type, with the binding's text defaults and none of an attribute binding's options: a string by the yaml scalar rules (so "1.0" or "yes" is quoted, and reads back as the same text) or as a json string, a number as number: "shortest" writes it, and true, false and null as such. A constant takes no part in reading: which rule reads an entry is decided by the rules' order and their when alone (section 5.1), so a binding keeps its when and its constants in agreement itself, and a round-trip fixture is what shows that a new entry is read by the rule that added it.
An add is planned by the first rule of its type, in the binding's order, that has an insert whose when holds. (FBL 0.7) In bullets an item's siblings are the items nested in the same parent, or the top-level items of the section when it has none: after-last, end and last-child put a new item after the last of them, start before the first, and section 6.3 says where that is in bytes.
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.
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. (FBL 0.7) A word a set adds, the value of a word slot that holds none or a new item of a many one, is an insert-key of one space and the word, right after the last word of the group held by that slot or by a slot listed before it in the rule's attributes on the same group, else at the start of the group. A removed word or item is a remove-key of the word and the whitespace before it, and a changed one a replace-value of the word, or of its group value. Items keep their order, and a new one is written after the slot's last. A set of such a slot writes the difference between the old list and the new one: the items of their longest common subsequence stay, an item removed and one added at the same place are one change, and the rest are removals and additions.
bullets (FBL 0.7). The family's new text, with the line endings and the last line without an ending as this section says above:
- A new item is one line, its indentation, its marker, one space and its rule's
emit, written as forlines. It takes the marker and the indentation of its previous sibling, else of its next sibling, else the marker of its parent and the parent's content column, else-at column 0. When the add gives the attribute of the rule'scontinuationslot a value that is not empty, each line of the value follows on a line of its own, indented with spaces to the new item's content column, an empty line of the value written as an empty line. - Where it goes. After the line span of its previous sibling; else at the start of the line span of its next sibling; else, as its parent's first nested item, at the end of the line ending of the parent's last line before its first nested item, which is its item line or the last line of its continuation; else, as the first item of a section that holds none, at the end of the line ending of the section's last line that is not blank, which is the heading when the section holds nothing else, with a line ending written first, so that a blank line stands before the item.
- A missing section is created by
insert.createwithat: "end-of-document"and the heading astext: oneensure-containerat the end of the body that writes a line ending, the heading and a line ending, so that a blank line stands before the heading; when the body's last line is blank already, or the body is empty, it writes the heading and a line ending only. The item that follows it at the same offset is the first item of a section that holds none. In a body whose last line has no line ending, each of the two splices also writes the line break that line lacks, before its text, and none after it: theensure-containerwrites two line endings and the heading, the item two line endings and its line, and the body still ends without a line ending. - A changed value of a group or a word is written as for
lines, in the item's text.re-emit-linerewrites the item's text, never its indentation or its marker. - The continuation. Its value span runs from the end of the item line, before its line ending, to the end of the last line of the continuation, before that line's ending. Setting it is a
replace-valueof that span with the new value, every line of it written after a line ending and indented with spaces to the item's content column, an empty line written empty; on an item without continuation it is aninsert-keyat the end of the item line; setting the empty text is aremove-keyof the span. - A move to another depth. The entry's line span is removed (
remove-entry) and inserted where a new item would go at its new place (insert-entry), every line of it that is not blank shifted by the difference between the column a new item's marker would have there and the column its marker had. A line is shifted right by writing that many spaces before it, and left by taking that many columns of whitespace off its start, a tab counting four columns; a tab the shift cuts into is replaced by the spaces that remain of it. The marker and every other byte stay as they were. Like every offset of an edit, the place of the insertion is taken in the body before the edit: a move to just after the entry's old parent is inserted at the start of the removed span.
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, and, with no sibling, where section 6.3 puts a new entry that has none), 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, re-indented in bullets (section 6.3), 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; (FBL 0.8) the add writes the new rule's constants (section 6.2). No other change writes a constant: a set, a move, a rename and the reference rewrites of section 5.7 leave a constant's key and value as they are, and a remove removes them with the entry. (FBL 0.7) In bullets, when the new type's rule has an emit, a retype is one re-emit-line of the item's text from that emit with the retype's values: the item's continuation and nested items stay where they are, and the host does not warn.
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.