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/2replaces 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/2cannot 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/2strips 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.pngnever touches an unrelated@a.png.bakreference).
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
@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.
@spec inline!(Lmml.Bundle.t(), String.t() | nil) :: Lmml.Bundle.t()
Same as inline/2, but raises on failure.
@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.
@spec pack!(Lmml.Bundle.t(), String.t() | nil) :: Lmml.Bundle.t()
Same as pack/2, but raises on failure.