Dextrin.Document (Dextrin v0.3.3)

Copy Markdown View Source

A .dxn document with everything it holds: its value, and every comment, blank line and comma around it (design/comment-retention.md).

Dextrin.decode/2 keeps only the value, which is right for data and wrong for a file people annotate: decoding, changing one entry and encoding again deletes every comment. A document keeps the full-fidelity syntax tree instead -- every token, trivia included (Ichor.Syntax) -- with the value as a projection of it:

{:ok, doc} = Dextrin.Document.parse(source)
Dextrin.Document.to_value(doc)   # what Dextrin.decode/2 returns
Dextrin.Document.render(doc)     # source, byte for byte

One grammar, parsed one way. The tree comes from the same priv/grammar/dxn.aether as decode/2, through the lossless entry point Ichor generates beside the fast one, and the same actions build the value -- so a document parses exactly when decode/2 would, and refuses with the same errors.

An untouched node renders its own bytes. Preservation is a property of rendering, not something a printer has to get right.

Edits

replace/3, delete/2 and insert/4 name what they change by a path from the document's value: a map's key as it decodes (:deps, "x"), a struct's field name ("y"), a list's, tuple's or set's index. Each splices the source text at the edited node and parses the result again, so every byte it does not touch is rendered as it was, and the edited document is checked by the same parser as any other.

A comment belongs to an entry (design/comment-retention.md, "Trivia ownership"), by lines alone: a block starting on an entry's own line is that entry's trailing block, however many lines it runs to; every other block of comment lines is above the next entry, and a blank line separates blocks. So a deleted entry takes its trailing block and the blocks above it, and nobody else's; a replaced value keeps every comment around it, even one it has made stale; an inserted entry goes above the comments of the entry it lands before, so they stay with that entry.

Updating to a new value

update/3 is a convenience over the edits, for a caller who has only the new value: it compares it with the document's, leaves what is equal alone -- 1.50 stays 1.50 when the new value is 1.5 -- and answers the edits it made as a changeset. Lists are compared with List.myers_difference/2. A deleted item followed by an inserted one is ambiguous: a replace keeps the deleted item's comments, a delete and an insert drop them. on_ambiguity: decides -- :safe (the default) takes it as a replace where the deleted item owns no comment, and refuses where it does; :guess always takes it as a replace; :error refuses every such pair.

Summary

Types

An edit update/3 made, in the order it made them.

Where a value sits in a document: one key, field name or index per level.

Why an edit was refused.

t()

A parsed document: its syntax tree, the value it holds, and the options it was parsed with, which every edit parses again with.

Functions

doc without the entry at path -- a map's key and value, a list's item -- and without the comments that belong to it: the blocks above it and the block trailing it.

doc laid out the way the pretty printer lays out a value, with every comment kept beside the entry it belongs to (Dextrin.Document.Formatter). Layout only: every token is written as it was. :indent is the spaces per level (2), :width the columns a nested collection's one line must fit in (80).

doc with value added to the collection at path: before the item at index in a list, tuple or set (-1 for the end), or under the new key in a map or a struct, after its last entry. It is laid out like the entries beside it. A key already there is refused; that is replace/3's.

Parses source, keeping everything in it. opts are Dextrin.decode/2's -- :registry, :trusted, :strict -- and mean the same; so do the errors.

doc as .dxn text: its source, byte for byte, when nothing was edited.

doc with the value at path replaced by value, written as Dextrin.encode/2 writes it -- laid out over lines at the indentation it replaces when it needs more than one. Every comment around it stays.

The value doc holds: what Dextrin.decode/2 gives for its source.

doc holding value, by the fewest edits that make it, and those edits. See "Updating to a new value" above for on_ambiguity:.

Types

change()

@type change() ::
  {:replace, {path(), term()}}
  | {:delete, path()}
  | {:insert, {path(), integer() | term(), term()}}

An edit update/3 made, in the order it made them.

path()

@type path() :: [term()]

Where a value sits in a document: one key, field name or index per level.

refusal()

@type refusal() ::
  {:not_found, path()}
  | {:ambiguous, path()}
  | {:exists, path()}
  | {:not_a_collection, path()}
  | Dextrin.Error.t()
  | [Dextrin.Error.t()]

Why an edit was refused.

t()

@type t() :: %Dextrin.Document{
  opts: keyword(),
  tree: Ichor.Syntax.Node.t(),
  value: term()
}

A parsed document: its syntax tree, the value it holds, and the options it was parsed with, which every edit parses again with.

Functions

delete(doc, path)

@spec delete(t(), path()) :: {:ok, t()} | {:error, refusal()}

doc without the entry at path -- a map's key and value, a list's item -- and without the comments that belong to it: the blocks above it and the block trailing it.

format(document, opts \\ [])

@spec format(t(), keyword()) :: String.t()

doc laid out the way the pretty printer lays out a value, with every comment kept beside the entry it belongs to (Dextrin.Document.Formatter). Layout only: every token is written as it was. :indent is the spaces per level (2), :width the columns a nested collection's one line must fit in (80).

insert(doc, path, where, value)

@spec insert(t(), path(), integer() | term(), term()) ::
  {:ok, t()} | {:error, refusal()}

doc with value added to the collection at path: before the item at index in a list, tuple or set (-1 for the end), or under the new key in a map or a struct, after its last entry. It is laid out like the entries beside it. A key already there is refused; that is replace/3's.

parse(source, opts \\ [])

@spec parse(String.t(), keyword()) ::
  {:ok, t()} | {:error, Dextrin.Error.t() | [Dextrin.Error.t()]}

Parses source, keeping everything in it. opts are Dextrin.decode/2's -- :registry, :trusted, :strict -- and mean the same; so do the errors.

render(document)

@spec render(t()) :: String.t()

doc as .dxn text: its source, byte for byte, when nothing was edited.

replace(doc, path, value)

@spec replace(t(), path(), term()) :: {:ok, t()} | {:error, refusal()}

doc with the value at path replaced by value, written as Dextrin.encode/2 writes it -- laid out over lines at the indentation it replaces when it needs more than one. Every comment around it stays.

to_value(document)

@spec to_value(t()) :: term()

The value doc holds: what Dextrin.decode/2 gives for its source.

update(doc, value, opts \\ [])

@spec update(t(), term(), keyword()) :: {:ok, t(), [change()]} | {:error, refusal()}

doc holding value, by the fewest edits that make it, and those edits. See "Updating to a new value" above for on_ambiguity:.