Documentation / Resources and file values

Resources and file values

Projects contain more than source. Images, fonts, stylesheets and data files need to reach the output, and the links pointing at them need to be correct from wherever they are read.

Moth handles this with one small surface. A file path names a file and evaluates to a String. The compiler validates the file and remembers which one you meant, until a builder decides where it lands and what the URL says.

Choose the explanation level: File paths

File paths

A file path tells the compiler that a file belongs to your project. Write the path with its extension, in the place where you would write any other value.

Naming a file

logo #= @images/logo.svg

@images/logo.svg resolves from the module root, so the same spelling works from any file in the module. The compiler checks that the file exists and is a real file, not a folder.

Extensions matter

An extension is required, and it tells the compiler what kind of file you mean.

logo  #= @images/logo.svg
intro #= @docs/intro.mtf

@images/logo has no extension and is rejected. .mtf and .md files give you their content. Every other accepted extension names a resource-bearing String, copied into your site when something you actually build uses it. .moth files are different: they hold declarations, so you bring those in with a dependency clause instead.

Linking to your own pages

A page on your site is a route, not a file the compiler emits. Write @/ for your site root.

docs #= [$html:<a href="[@/]docs/">Docs</a>]

@/ names no file. If your project sets an origin, @/ includes it, so a site served from a subfolder keeps working without you repeating that setting in every link.

URLs stay strings

A web address somewhere else is not a project file, so write it as an ordinary string.

external #= "https://example.com/logo.svg"

The compiler leaves strings alone. It never copies, rewrites or watches them.

A file path is an explicit-extension path written in expression position. Physical-target-bearing file paths name one regular file that the project owns. An accepted file path evaluates to a String; see File values for what that string contains.

Together with dependency clauses, which bring in source and provider files, file paths are how the compiler learns that a file participates in a build. Nothing scans rendered HTML, CSS, Markdown or arbitrary strings to rediscover files.

Syntax

A file path is an ordinary expression.

logo  #= @images/logo.svg
font  #= @fonts/cmunss.woff2
intro #= @docs/intro.mtf

icons {String} = {
    @icons/add.svg,
    @icons/remove.svg,
}

A file path:

  • carries one explicit extension on its final component
  • resolves to an existing regular file
  • resolves from the owning module root rather than the physical source-file directory
  • cannot be followed by dependency selections
  • is not a directory
  • is not an external URL

The explicit extension is what distinguishes one file value from a declaration namespace. .mtf and .md name content files and evaluate to their content. Every other accepted extension names a resource. .moth is the one recognised extension with no file value, because source declarations are consumed through dependency clauses. It is retained as a SourceKindNoFileValue structural diagnostic fact, not as a physical target.

Context decides the meaning

The same explicit-extension spelling means different things in different positions.

  • In a dependency clause it names an already registered provider target.
  • In expression position it names one file value.
@vendor/drawing.js as drawing

drawing_url = @vendor/drawing.js

The first binds the provider's namespace. The second is the file, as a resource string. These two owners never reinterpret one another's path family: a dependency clause does not become a file value, and a file value never exposes a provider namespace or invokes provider declaration semantics.

Dependency-clause source rules are unchanged. @docs/intro content is the clause form and @docs/intro.mtf content stays a diagnostic, because dependency clauses require extensionless source paths. See Dependency paths.

Resolution

Resolution starts at the owning module root, exactly like dependency paths. The physical directory of the declaring file has no effect.

-- in src/internal/deep/renderer.moth, whose module root is src/
logo #= @images/logo.svg

That resolves to src/images/logo.svg.

Each physical-target-bearing file path is resolved once. Repeating it in the same module reuses the already resolved result. A SourceKindNoFileValue occurrence has no physical target and is not resolved.

Filesystem ownership

A file path may address only a regular file inside the current module or package's private filesystem ownership.

  • traversal starts at the owning module root
  • ordinary unrooted directories owned by that module may be traversed
  • reaching a child normal module or a support package stops traversal
  • another module's private file cannot be addressed directly, including a private .mtf
  • the project package facade is not a global resource escape
  • support-package visibility follows the existing scoped-package rules
  • canonical containment rejects symlink escape
  • strict case validation reuses the source-path policy

Cross-module content and resources travel through exported String constants or reachable function implementations. There is no public visibility table for files themselves.

Rejected spellings

These are diagnostics rather than file values:

  • an absolute-root path such as @/logo.svg
  • @./ and parent components such as ..
  • @@name
  • a missing extension
  • a .moth file value
  • a directory target
  • a missing target
  • a path followed by dependency selections

A file path in config.moth is also rejected. Config is compiled before the source graph exists, so it cannot name a build input by path.

Site-root URLs

Bare @/ is the site-root URL. It is a String and it names no file.

home #= [$html:<a href="[@/]">Home</a>]
docs #= [$html:<a href="[@/]docs/">Docs</a>]

A resource URL and a site-root URL answer different questions. A resource URL is written relative to the artefact that observes it and never carries the project origin. A site-root URL is absolute and always carries it, because it addresses a route rather than a file the build emits.

@/ renders the project's configured origin, so a site served from a subpath keeps working without repeating that setting in every link. With no origin configured it renders /.

A site-root URL has no resource identity. It is never checked, copied, hashed, rewritten, watched or included in a resource union.

Only the bare spelling is a site-root URL. @/logo.svg is a rejected absolute-root path, because an absolute root would escape module ownership. That is the same rule a dependency clause applies, and @/ remains invalid in a dependency clause for the same reason.

Moth-aware Markdown links share that site root. A $md link target beginning with a single /, such as @/docs/packages (Packages), renders through it and picks up the origin without spelling the site root out. See Markdown formatting.

External URLs

These remain ordinary untracked strings:

external  #= "https://example.com/logo.svg"
cdn       #= "//cdn.example.com/app.js"
favicon   #= "/favicon.svg"

They are not checked, copied, rewritten, watched or included in resource unions. Write a URL as a string when you mean a URL.

Every physical-target-bearing path is a build input

A physical-target-bearing file path is a build dependency wherever it is authored, before any question of reachability. This includes a path in:

  • an unused private constant
  • a helper template that is never rendered
  • either branch of an ordinary if
  • a branch later removed by static Bool specialisation
  • a generic template that is never materialised
  • a function that is unreachable from the selected entry

In every one of those cases the file is resolved, validated and watched, and an error inside a referenced content file is still reported. A SourceKindNoFileValue .moth occurrence has no physical target, semantic-source membership, source or watch record, and is never resolved. Being an input and reaching an output are separate facts: a resource that no reachable output uses is never read, hashed or emitted.

Resource identity

Each resolved resource has one stable semantic origin: its owner plus its owner-relative logical path.

Identity deliberately excludes absolute paths, output paths, routes, URLs, content hashes, aliases, export bindings and source locations.

The observable consequences are:

  • moving a declaration between ordinary files in one module does not change resource identity
  • moving or renaming the resource itself does change resource identity
  • consumer-local aliases and re-exports preserve identity
  • changing a file's bytes does not change its identity

Related concepts

Choose the explanation level: File values

File values

Write a file path where you would write any other value, and you get a String back.

logo    #= @assets/logo.svg
intro   #= @docs/intro.mtf

logo is the link to your image. intro is the text of your Moth template file. Both are ordinary strings.

Putting a file in a template

image #= [$html:
    <img src="[@assets/logo.svg]" alt="Moth">
]

image is a normal String. You can compose it, store it in a constant, pass it to a function and return it, exactly like any other string.

The compiler writes the URL

You never write the output path yourself. The builder decides where the file is emitted, then writes the link from the page that contains it.

The same logo becomes ./assets/logo.svg on the home page and ../../assets/logo.svg on a page two folders deep. That is why the compiler holds on to which file you meant instead of turning the path into text straight away.

Content files give you their content

A .mtf or .md path gives you what the file says, not where it lives.

license #= @legal/license.md

page #= [$html:
    <footer>[license]</footer>
]

A .moth path gives you nothing: source files hold declarations, and declarations are brought in with a dependency clause.

@helpers build_value

Unused files still count

A file path is a build dependency wherever you write it, even in a constant nothing reads. The compiler checks it exists and watches it for changes.

Reaching the output is separate. A file that no page ends up using is never copied into your site.

A file path written in expression position evaluates to a String. There is no Path type. The file kind decides how that string is built, and the compiler keeps enough structure inside it to write correct links later.

The file kind decides the value

intro   #= @docs/intro.mtf
license #= @legal/license.md
logo    #= @assets/logo.svg
  • a .mtf path evaluates to that file's compiler-owned content string
  • a .md path evaluates to that file's compiler-owned content string
  • any other accepted file evaluates to a resource-bearing string
  • a .moth path has no file value at all

Every accepted file value above has language type String. Nothing exposes a file object, a filesystem handle or an output path to the program.

Content files reuse their content constant

A direct .mtf or .md value is the same folded content constant a dependency clause binds. The file is compiled once, by its normal owner.

@docs/intro content as intro

same #= @docs/intro.mtf

Both forms name one compiled file and one content value. The direct form is convenient when a page needs the content once and does not need a bound name. The dependency clause is the form to use when the name matters or when the file is used repeatedly.

Whichever form is used, plain Markdown links and images inside a .md file stay literal and untracked.

.moth files have no file value

source = @helpers.moth

That is rejected. Preparation records the spelling as SourceKindNoFileValue, a structural diagnostic fact with no physical target. Stage 0 does not resolve or validate a .moth target, add semantic-source membership or create a physical-source or watch record. No filesystem target resolution occurs. AST always issues MothFileHasNoValue, whether or not a matching .moth file exists.

A .moth file exposes declarations, and declarations are consumed through dependency clauses:

@helpers build_value

Source text is never exposed as a string. A source kind may be a file value only where the language defines one canonical content string for it, which is true for .mtf and .md and nothing else.

Resource strings are ordinary strings

A resource file value is a normal String, so ordinary string rules apply.

logo = @assets/logo.svg

logo_for |name String| -> String:
    if name is "main":
        return @assets/main.svg
    else
        return @assets/fallback.svg
    ;
;

logos {String} = { @assets/main.svg, @assets/fallback.svg }

It may be mutable, a parameter, a return, an optional, a collection element, a map value, an exported constant or a runtime value. Nothing is rejected merely because a string names a resource.

The URL is not decided yet

Internally a resource file value is not the text "./assets/logo.svg". It carries a resource anchor, and the string stays structural until a builder knows which artefact will contain the text.

image #= [$html:
    <img src="[@assets/logo.svg]" alt="Moth">
]

image is a fully folded compile-time constant even though its final characters are unknown. Composition, storage, export and template insertion all work on it.

The consequences are:

  • output paths and URLs are never observable through a cast, formatter or public interface
  • a compile-time operation that needs the final characters is rejected while placement is unresolved
  • runtime text operations happen only after the containing artefact has resolved each anchor

There is no second source-visible string type. A resource-bearing string is a String.

URLs are rendered by the builder

Resource identity, byte source, emitted output path and rendered URL are four separate facts. The builder chooses the output path, then chooses the artefact whose URL rules observe the rendered text, and renders a relative URL from there.

resource: assets/logo.svg
context:  index.html
URL:      ./assets/logo.svg

resource: assets/logo.svg
context:  docs/getting-started/index.html
URL:      ../../assets/logo.svg

resource: styles/fonts/site.woff2
context:  styles/site.css
URL:      ./fonts/site.woff2

The URL context is the artefact whose URL resolution rules observe the string, not necessarily the file that contains the generated code. Page runtime code uses the active page document, standalone CSS uses the stylesheet, and inline CSS uses the page document.

Project HTML origin configuration is never prepended to a resource URL. Resource URLs are relative to their context artefact.

Site navigation is the separate case. A route is not a file the build emits, so it cannot be written relative to a context artefact and does need the origin. @/ is the origin-aware site-root string. See File paths.

Crossing a module boundary

A file value obeys filesystem ownership, so it cannot reach into a child module's or support package's private files. Content and resources cross a boundary as ordinary exported strings instead:

export:
    logo  #= @assets/logo.svg
    intro #= @docs/intro.mtf
;

A consumer binds those as ordinary exported String constants. Aliases and re-exports preserve the resource behind the string, so the link still renders correctly from the consumer's own pages.

Moth templates and Markdown

.mtf files remain declarationless and dependency-clause-free. A file value is used through a nested Moth template inside the implicit Markdown body.

[$html:
    <img src="[@images/ownership.webp]" alt="Ownership graph">
]
  • plain @images/ownership.webp Markdown text remains ordinary text
  • a nested file path resolves from the .mtf file's owning module root
  • .mtf gains no frontmatter and no general declarations
  • plain .md link and image targets remain literal and untracked

Rendered Markdown and HTML are never scanned to discover files.

Direct Moth-template compilation

The direct .mtf compiler service has no route and no containing output artefact, so it cannot render final resource URLs.

It returns structural folded content with its resource facts. Extracting plain text succeeds only when the folded content contains no unresolved anchors; otherwise the request reports unsupported content rather than guessing a path. A project builder consumes the structural result and applies its normal resource link plan.

Static branch specialisation

Both branches of an ordinary if complete normal validation, and a physical-target-bearing path in a branch removed by known-Bool specialisation is still resolved and validated. A SourceKindNoFileValue occurrence still reaches AST for its unconditional diagnostic but is never resolved as a physical target. That inactive branch contributes no output. A runtime branch retains ordinary reachability, so any reachable physical-target-bearing path contributes its uses. See File paths for why being a build input is separate from reaching an output.

Related concepts