6. Writing
Contents of FBL 0.15
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), child element holding a value (xml create), or (FBL 0.11) absent part (grammar 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; (FBL 0.11) a part and the whitespace before it (grammar) |
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; (FBL 0.11) grammar: nothing, after the entry's last token and before a trailing comment | {; and a closing } line indented like the statement; (FBL 0.11) grammar: opens.open and opens.close |
blocks, grammar |
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. (FBL 0.10) An attribute bound with child (section 5.2) that the add gives a value is written in the new entry under the levels of child, which the entry gets as a set creates them (create, section 5.2): the outermost level's key at its place in keys when keys names it, else after the entry's other keys and before its constants and skeleton, each deeper level one indentation step deeper (section 6.3) and the attribute's own key in the innermost, all in the entry's one insert-entry; attributes whose child reach one level share it, in the order of their bindings. In yaml a level is its key with a block mapping below it, in json a member whose value is an object. So a library added to a YAML pipeline is - notebook: with path: one step deeper below it, and reads back as the library added. |
emit |
The text of a new entry for families without keys (lines, blocks, xml, bullets, (FBL 0.9) grammar, where a line feed in it starts a new line, section 6.3), 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. |
refusals |
(FBL 0.10) {when, container}, each a LocalizedText: the sentence a host shows when the add is refused because when does not hold (when), or because the container is missing and the insert has no create (container); (FBL 0.14) and, for an insert that adds a member (below), because the add gives no key or an empty one (key), or a key the mapping already holds (duplicate); section 6.4. |
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.10) among the rules whose origins admit the reading the add is made in (section 5.1). (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.
A new member, named by its key (FBL 0.14). In the yaml and json families, a rule whose at ends in a {c} segment reads the members of a mapping, each named by its key (/resources/jobs/{job}). Its insert adds a member to the container, which is that mapping: the new member's key is the value the add gives the attribute the rule binds to {capture: c} (section 5.2), and a host MUST take it as the new entry's key rather than as a set of the attribute, so that a readOnly on that binding, which refuses a rename, does not refuse the add. The key is written as section 6.3 writes a new string value: in yaml a scalar, plain when it is plain-safe and else in text.quote, followed by : and the line ending, the member's value below it one indentation step deeper, holding the entry's keys in keys order, its constants and its skeleton, as a new mapping item's are written; in json a string, : and an object holding them, written one member per line as its siblings are. The member goes where place puts it among the container's members: after-last right after the line span of the rule's last member, so that a new job follows the last job, before the container's members of any other rule; end after the container's last member; start before its first; {before: key} before that member. It is indented like the member it follows, else like the one it precedes, else one step deeper than the container's key (section 6.3). A container that is missing is created as create says, with its missing levels as below, so that create: {"at": "end"} on /resources/jobs writes resources: at the end of the body when it is missing and jobs: at the end of resources; a container written as its key alone (jobs:) takes the member as the first of a block mapping below the key, and one written in flow form (jobs: {}) takes no insert, as a flow mapping does not (below). An add that gives the key attribute no value or the empty text is refused, and so is one whose key the container already holds, compared as written text (section 4.1.5), since a mapping holds one key once; their sentences are the insert's refusals.key and refusals.duplicate (section 6.4). The member is one insert-entry splice of its key and its value, after the ensure-container splices of the levels it creates, all at one offset, and its undo removes exactly those bytes. Removing such an entry removes its line span, its key's line and every line of its value, with one separator in json, as for any entry; the rule's remove.container decides what becomes of a container it leaves empty, remove-empty-levels removing jobs: and then resources: when nothing is left in them. A rule with such an insert MUST bind an attribute to {capture: c} (section 14.1).
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.
Flow sequences (FBL 0.10). In yaml, a container that is a flow sequence (section 4.3) is written into so:
- An empty flow sequence (
tasks: [], nothing but whitespace between its brackets) takes an insert as areplace-valueof the value and the whitespace between it and its key's:by nothing, followed by theinsert-entryof the new entry as the first item of a block sequence under the key: after the line ending of the key's line, so after a comment on that line, which stays where it is, written as a new yaml sequence item under a key with no sibling (section 6.3), with the line endings of section 6.3. Both splices are one edit. - A one-line flow sequence (
depends_on: [{task_key: a}]) takes an insert as oneinsert-entryright before its], of,and the new item, after its last item; withplace: "start", right after its[, of the item and,. A mapping item is written in flow form,{its members}, each memberkey: valuewith,between them, its keys ininsert.keysorder and each value by section 6.3's yaml scalar rules, a value that is not plain-safe or holds,,[,],{or}being quoted; any other item is its value alone. - A removal of an item of a flow sequence is a
remove-entryof the item and one separator: the,after it and the whitespace up to the next item, or, for the last item, the,before it and the whitespace between that separator and the item, as in json (below). The only item is removed alone, leaving[], andremove-when-emptythen removes the container as for any container left empty. - A flow sequence written over several lines, and a flow mapping, take no insert: an add into one is refused with a reason (section 6.4), and nothing is written.
Each of these has its exact inverse (section 7.1): the undo of an insert into an empty flow sequence puts back the value and the whitespace before it and removes the entry, so the body is the bytes it was.
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.
grammar (FBL 0.9). The family's new text, with the line endings and the last line without an ending as this section says above:
- A changed value is a
replace-valueof the value's span (the groupvalueof the slot'smatch, or the node's own span) with the attribute's new value as text, unescaped and unquoted, unless (FBL 0.11) the slot hasescape, in which case everyquoteandwithcharacter of the value is written afterwith(section 4.9); the family's read-again check (section 4.9, Writing) still applies, and refuses a value the escape cannot write, such as one holding a line break. A number is written as section 6.3's Numbers says,trueandfalseas such. - A new entry is its rule's
emit, with its placeholders replaced as forlines, where each line feed inemitstarts a new line: it is written as the line ending this section chooses, followed by the new entry's indentation. An entry's indentation is the whitespace before it on its first line. - Where it goes.
after-lastfollows the last entry of the rule in document order,endthe last child of the container,next-siblingthe entry the gesture was made on;startprecedes the container's first child. The container is the nodeinsert.containerselects, the root when it is absent. A new entry that follows an entry goes after that entry's line span, on a line of its own, with its indentation, and ends with a line ending; when the entry followed does not end its line, the new entry is written right after its own span, after a line ending and the indentation of the line the followed entry ends on, and ends with nothing. A new entry that precedes a child goes at the start of that child's line span, with its indentation, followed by a line ending. A new entry with nothing to follow or precede, andend-of-document, goes after the last byte of the body: on a line of its own after a body whose last line is empty or which is empty, else after a line ending, with no indentation, the body keeping or lacking its final line ending. - A removed entry is a
remove-entryof its line span, or of its own span when it does not start and end its lines. - A reference is rewritten by
rewrite-referenceof its value's span (section 5.7). - (FBL 0.12) An item of a slot with
items(section 5.2). A changed value of one item is areplace-valueof that value's span, and nothing else of the list changes. A new item is aninsert-key, right after the last item, ofseparatorand the slot'screate.emitwith each placeholder an item attribute's value:,[{visibility},{maturity}]after[0.08,0.79]. A removed item is aremove-keyof the item's span and one separator: the one before it, or, for the first item, the one after it. A set of the attribute writes the difference between the old list and the new one as amanyslot does (section 5.2), so an item that is not changed keeps its bytes. In an insert'semit, the placeholder of anitemsattribute is every item written bycreate.emit, joined byseparator. - (FBL 0.15) A term of a slot with
expandis areplace-valueof the node's own span, written as follows: a text of the slot'skeywordsthat stands for the IRI; elseprefix:localunder the declaration with the longest IRI that the new IRI starts with and is longer than, a later declaration winning a tie, whether or not it is in force at that place, when what remains of the IRI matcheslocalwhole; else the IRI between<and>. In an insert'semita term's placeholder is written so. So renaming a term writes it in the file's own prefixes, and a writer never invents a declaration. The family's read-again check (section 4.9) applies, and refuses a term that would read otherwise there. - (FBL 0.15) A string of a slot with
escapeandrequote: a set is areplace-valueof the whole node the slot reaches, with the value, escaped asescapesays, between two ofrequote, so that'A'or"\""A"\""set toBbecomes"B", and what follows the node, such as a language tag, stays. - (FBL 0.13) An item of a slot with
split(section 4.9). A set of the attribute writes the difference between the old list and the new one as amanyslot does (section 5.2): 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. A changed item is areplace-valueof the item's span with the new value, escaped as the slot'sescapesays, and no other byte of its node changes. A new item is aninsert-key, right after the last item the slot reaches, ofseparatorand the value, so that"Extra, More"givenMostbecomes"Extra, More, Most"; where no node the slot reaches holds an item, the new items, joined byseparator, are areplace-valueof the value's span of the last node it reaches (""becomes"Extra, More"), and where it reaches no node, they are the{value}of the one node the slot'screatewrites. A run of removed items that follows an item the set keeps in the same node is aremove-keyfrom the end of that item to the end of the run's last item, so that the separator and the white space before each removed item go with it:"One ,Two, Three"withoutTwobecomes"One, Three". A run at the start of a node, before an item the set keeps, is aremove-keyfrom the start of the run's first item to the start of that kept item:"Extra, More"withoutExtrabecomes"More". A node whose every item the set removes is removed as a node of amanyslot withoutsplitis, by aremove-keyof the node and the whitespace before it. Every byte of an item the set keeps, and between two items it keeps, stays. In an insert'semit, the placeholder of asplitattribute is its items joined byseparator.
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. (FBL 0.15) Two splices of one edit that replace one span with one text are one splice, and two that replace it with different texts refuse the edit (section 4.9). 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.10) A binding gives the sentence of a refusal it causes, as readOnly gives one (section 3.4): an attribute binding's refusals.empty for empty: "refuse" on a set or on an add, its refusals.absent for an absent refusal, an insert's refusals.when for a when that does not hold, and its refusals.container for a container that is missing when the insert has no create (section 6.2). A host MUST show that sentence as the reason of the refusal, in the user's language as a LocalizedText gives it; without one, the wording is the host's.
(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 position {after: id} naming an entry of another type at the same level is one insert-entry on a line of its own right after that entry's line span, so that a statement can stand beside a header it is no sibling of. 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.