Lmml. Bundle
(Lmml v0.1.0)
View Source
A loaded lmml entity, regardless of which of the two on-disk forms it
came from:
.lmml-- a bare, self-contained Markdown-superset text file. There is nowhere for an external reference to point to, so every embed it mentions must be inline..lmmlz-- a zip archive whose canonical narrative entry is named by stripping the trailingzfrom the archive's own filename (foo.lmmlzcontainsfoo.lmml), plus whatever other entries its@name.extreferences point at.
open/1 detects which form a file is by sniffing its content (the zip
local-file-header magic bytes), not by trusting its extension, and
either way returns a Bundle exposing the exact same API: narrative/1,
embeds/1, embed/2, entries/1.
Summary
Types
Which on-disk form this bundle was loaded from (or is destined for).
A single issue found by validate/1.
Functions
Resolves a named embed's actual content: inline embeds return their
own captured text directly (working for any bundle kind); external
references are looked up among the zip's entries, which only exists
for a :zip bundle.
Every embed mentioned in the narrative (see Lmml.Document's embeds field).
Names of the zip entries this bundle carries, excluding the narrative itself. Always [] for a :text bundle.
The bundle's own logical name (always ending in .lmml, regardless of bundle kind).
The bundle's raw narrative text.
Builds a bare-text bundle directly from narrative text, without touching
disk. name is normalized to always end in .lmml (the logical
narrative name is the same regardless of file form).
Builds a zip-backed bundle directly from narrative text plus a map of
external entries (%{"image.png" => <<bytes>>}), without touching disk.
Every entry name is path-safety validated the same way open/1 validates
entries read from a real archive.
Opens a lmml entity from disk, auto-detecting whether path is a bare
text narrative or a zip archive by sniffing its content.
Same as open/1, but raises on failure.
True for a bundle backed by a bare text narrative (no external files possible).
Cross-checks a bundle's narrative against its own entries and its own
internal consistency. This is a distinct, opt-in step from open/1 or
new_zip/3 (which only ever reject an entry name that is outright
unsafe -- see validate_entry_names/1): validate/1 instead catches
authoring mistakes that are still perfectly well-formed as far as
parsing and path-safety go. Every issue found is returned at once,
rather than stopping at the first
Validates that none of names could escape the bundle root when
resolved: no absolute paths, no .. path segments. Used both when
opening a zip archive (every entry it contains) and when resolving a
single reference at read time.
Persists a bundle to disk in its current form (text stays text, zip
stays zip) -- this does not convert between forms; see Lmml.Pack for
that. For a :zip bundle, the internal narrative entry name is always
freshly derived from path's own basename (not from whatever name the
bundle was constructed/opened with), so whatever you write is always
internally self-consistent with its own filename.
True for a bundle backed by a zip archive.
Types
@type kind() :: :text | :zip
Which on-disk form this bundle was loaded from (or is destined for).
@type validation_issue() :: {:missing_reference, String.t()} | {:orphaned_entry, String.t()} | {:malformed_embed_name, String.t()} | {:conflicting_embed, String.t()}
A single issue found by validate/1.
Functions
Resolves a named embed's actual content: inline embeds return their
own captured text directly (working for any bundle kind); external
references are looked up among the zip's entries, which only exists
for a :zip bundle.
@spec embeds(t()) :: [Lmml.Embed.t()]
Every embed mentioned in the narrative (see Lmml.Document's embeds field).
Names of the zip entries this bundle carries, excluding the narrative itself. Always [] for a :text bundle.
The bundle's own logical name (always ending in .lmml, regardless of bundle kind).
The bundle's raw narrative text.
Builds a bare-text bundle directly from narrative text, without touching
disk. name is normalized to always end in .lmml (the logical
narrative name is the same regardless of file form).
@spec new_zip(String.t(), binary(), %{optional(String.t()) => binary()}) :: {:ok, t()} | {:error, term()}
Builds a zip-backed bundle directly from narrative text plus a map of
external entries (%{"image.png" => <<bytes>>}), without touching disk.
Every entry name is path-safety validated the same way open/1 validates
entries read from a real archive.
Opens a lmml entity from disk, auto-detecting whether path is a bare
text narrative or a zip archive by sniffing its content.
Same as open/1, but raises on failure.
True for a bundle backed by a bare text narrative (no external files possible).
@spec validate(t()) :: :ok | {:error, [validation_issue()]}
Cross-checks a bundle's narrative against its own entries and its own
internal consistency. This is a distinct, opt-in step from open/1 or
new_zip/3 (which only ever reject an entry name that is outright
unsafe -- see validate_entry_names/1): validate/1 instead catches
authoring mistakes that are still perfectly well-formed as far as
parsing and path-safety go. Every issue found is returned at once,
rather than stopping at the first:
{:missing_reference, name}-- an@name.extreference has no matching zip entry. For a:textbundle this fires for every external reference, since a bare.lmmlhas no entries at all to resolve one against.{:orphaned_entry, name}-- a zip entry that no embed in the narrative actually mentions.{:malformed_embed_name, name}-- an embed name (inline or external) that could not safely become a zip entry name (seevalidate_entry_names/1). This applies to every embed, not only inline ones, sinceLmml.Pack.pack/2(planned) can turn any inline embed into a real zip entry later.{:conflicting_embed, name}-- two or more embeds share a name but disagree on content (including disagreeing on inline-vs-external), which is an authoring mistake: a name is meant to identify one entity consistently throughout a narrative.
Returns :ok when none of the above apply.
Validates that none of names could escape the bundle root when
resolved: no absolute paths, no .. path segments. Used both when
opening a zip archive (every entry it contains) and when resolving a
single reference at read time.
Note: for archives that actually went through :zip.unzip/2 (i.e. every
entry reaching this check via open/1), Erlang's own zip implementation
has already sanitized traversal/absolute entry names down to a bare
basename (logging a warning as it does so) by the time this runs -- so
in practice this check rarely has anything to catch on that path. It
remains meaningful as the only line of defense for entries that never
go through :zip at all, i.e. those supplied directly to new_zip/3.
Persists a bundle to disk in its current form (text stays text, zip
stays zip) -- this does not convert between forms; see Lmml.Pack for
that. For a :zip bundle, the internal narrative entry name is always
freshly derived from path's own basename (not from whatever name the
bundle was constructed/opened with), so whatever you write is always
internally self-consistent with its own filename.
True for a bundle backed by a zip archive.