Lmml.Pack (Lmml v0.1.0)

View Source

Converts between lmml's two on-disk forms by moving each embed between being carried inline in the narrative text (@@@name ... @@@) and being carried as a real zip entry (@name) -- the mechanical operation the .lmml <-> .lmmlz conversion boils down to, per the "Embed is the one abstraction" design decision (see docs/LANGUAGE_REFERENCE.md).

How the narrative text is rewritten

Md is a parser with no matching generator, so neither direction re-serializes the whole AST back into Markdown from scratch; both instead perform targeted substring surgery on the exact fence/reference occurrences already known from the parsed Lmml.Document, which is sufficient since embeds are the only lmml-specific syntax either direction ever touches:

  • pack/2 replaces each inline embed's entire, exact fence substring ("@@@name\ncontent@@@") with its reference form ("@name"). A fence's distinctive three-@ marker makes an accidental collision with unrelated prose implausible in practice (the only way to get a false match is for some other embed's own content to literally quote this whole fence, name and content both, byte for byte -- a pathological case treated as an accepted, documented limitation rather than something guarded against).
  • inline/2 cannot simply substitute "@@@name content@@@" in place of "@name": a fence's opening @@@ is only valid at the start of the document or immediately after a blank line (the same block-level rule every fenced construct in this language follows), so a reference used mid-sentence -- the common case -- would produce an unparseable result if replaced in place. Instead, inline/2 strips the bare @ sigil from every in-prose reference occurrence ("@notes.txt" becomes plain "notes.txt", no longer special syntax) and appends one proper, blank-line-separated "@@@name ... @@@" block per resolved reference at the end of the narrative. Substitutions only ever happen within segments outside any existing inline embed's own fence (never inside one), and use a word-boundary-aware regex so a short reference name is never matched as a prefix of a longer, unrelated token (e.g. stripping @a.png never touches an unrelated @a.png.bak reference).

Neither direction touches embeds it isn't converting: pack/2 leaves any already-external @name reference exactly as it is (there is nothing to move for it, though its bytes -- if resolvable -- are still carried forward into the result's entries, so packing an already-:zip bundle correctly merges its existing entries rather than losing them); inline/2 leaves any already-inline @@@name ... @@@ block exactly as it is.

Only text can be inlined

inline/2 fails with {:error, {name, :not_utf8}} for any external reference whose resolved content isn't valid UTF-8. A narrative is itself UTF-8 text, re-parsed as lmml/Markdown from scratch every time it's opened, so splicing genuinely binary content (a real image, say) into it isn't just ugly -- it actively crashes Md's parser, which matches one UTF-8 codepoint at a time and has no defined behavior for an invalid byte sequence appearing mid-text. A binary asset should stay external via @name.ext, which is exactly what .lmmlz archives are for; only text-like embeds (settings, notes, diffs, JSON, ...) are ever valid candidates for inlining.

Summary

Functions

Inlines every external reference in bundle into a @@@name ... @@@ block, returning a new :text bundle. name overrides the resulting bundle's logical name; defaults to bundle's own name.

Same as inline/2, but raises on failure.

Externalizes every inline embed in bundle into a real zip entry, returning a new :zip bundle. name overrides the resulting bundle's logical name; defaults to bundle's own name.

Same as pack/2, but raises on failure.

Functions

inline(bundle, name \\ nil)

@spec inline(Lmml.Bundle.t(), String.t() | nil) ::
  {:ok, Lmml.Bundle.t()} | {:error, term()}

Inlines every external reference in bundle into a @@@name ... @@@ block, returning a new :text bundle. name overrides the resulting bundle's logical name; defaults to bundle's own name.

Unlike pack/2, every external reference must actually resolve -- there is no such thing as a partially-inlined result, since a :text bundle has nowhere left to point a dangling reference at. A zip entry that no embed in the narrative mentions at all (see Lmml.Bundle.validate/1's {:orphaned_entry, ...}) is necessarily dropped, since it was never part of the document model to begin with and a :text bundle cannot carry an unreferenced entry.

inline!(bundle, name \\ nil)

@spec inline!(Lmml.Bundle.t(), String.t() | nil) :: Lmml.Bundle.t()

Same as inline/2, but raises on failure.

pack(bundle, name \\ nil)

@spec pack(Lmml.Bundle.t(), String.t() | nil) ::
  {:ok, Lmml.Bundle.t()} | {:error, term()}

Externalizes every inline embed in bundle into a real zip entry, returning a new :zip bundle. name overrides the resulting bundle's logical name; defaults to bundle's own name.

An already-external reference is carried forward as-is in the narrative text, with its bytes resolved against bundle itself and copied into the result's entries when possible -- this is what makes packing an already-:zip bundle a safe, idempotent merge rather than a lossy re-creation. A reference that isn't resolvable in the source bundle (a dangling @name.ext in a bare .lmml) is left dangling in the result too, since there is nothing to carry forward for it.

pack!(bundle, name \\ nil)

@spec pack!(Lmml.Bundle.t(), String.t() | nil) :: Lmml.Bundle.t()

Same as pack/2, but raises on failure.