textprompts
Elixir port of the cross-language textprompts toolkit. Load Markdown prompt files with TOML or YAML frontmatter, validate placeholders at format time, slice documents into anchored sections, and save them back. Output matches the Python, Go, TypeScript, and Julia ports against the shared testdata/sections/cases.json fixtures.
Install
def deps do
[{:textprompts, "~> 0.1"}]
end
Optional default mode in config/config.exs:
config :text_prompts, metadata_mode: :allowQuickstart
File.write!("greet.md", """
---
title = "Greeting"
version = "1.0.0"
description = "Demo prompt"
---
Hello {name}, welcome to {place}.
""")
prompt = TextPrompts.load!("greet.md")
ps = TextPrompts.PromptString.new(prompt.prompt)
{:ok, out} = TextPrompts.PromptString.format(ps, name: "Ada", place: "Earth")
# "Hello Ada, welcome to Earth.\n"Sections and TOC
doc = """
# Overview
Intro.
## Setup
Run `mix deps.get`.
## Usage
Call `TextPrompts.load!/1`.
"""
result = TextPrompts.parse_sections(doc)
Enum.map(result.sections, & &1.anchor_id)
# ["overview", "setup", "usage"]
{body, true} = TextPrompts.get_section_text(doc, "setup")
IO.puts(TextPrompts.render_toc(result, "doc.md"))TextPrompts.load_section/3 chains the loader with get_section_text/2.
Frontmatter
TOML is tried first, YAML is the fallback. Unknown keys land in meta.extras. Force a format on save with format: :yaml | :toml.
"""
---
title: Greeting
version: 1.0.0
tags: [demo, example]
---
Hi {name}.
"""Metadata modes
| Mode | Behaviour |
|---|---|
:strict |
Frontmatter required; title, version, description must be non-empty. |
:allow | Default. Frontmatter optional; parsed when present. |
:ignore |
Never parses frontmatter; strips a leading --- block; title from filename. |
{:ok, _} = TextPrompts.load("no_meta.md", meta: :allow)
{:error, %TextPrompts.Error.MissingMetadata{}} =
TextPrompts.load("no_meta.md", meta: :strict)
TextPrompts.with_metadata(:ignore, fn -> TextPrompts.load!("doc.md") end)Save
prompt = TextPrompts.load!("greet.md")
updated = put_in(prompt.meta.version, "1.1.0")
:ok = TextPrompts.save("greet.md", updated)save/3 accepts a %TextPrompts.Prompt{} (emits frontmatter when metadata is non-empty) or a raw string (writes body only). Default emit format is the source format if known, else :toml.
~P sigil
defmodule MyPrompts do
use TextPrompts.Sigil
def greet, do: ~P"Hello {name}, welcome to {place}."
end
The sigil compiles the PromptString (raw template + placeholder set) at compile time when the body is a literal.
Cross-language compatibility
| Language | Package |
|---|---|
| Python | textprompts (canonical) |
| Elixir | textprompts |
| Go | textprompts-go |
| TypeScript | textprompts-ts |
| Julia | TextPrompts.jl |
Shared fixtures: testdata/sections/cases.json. The fixture parity test in test/text_prompts/sections/fixture_test.exs asserts deep equality with the canonical JSON output.
Telemetry
Every load and save runs through :telemetry.span/3:
| Event | Measurements | Metadata |
|---|---|---|
[:text_prompts, :load, :start] | system_time, monotonic_time | path, mode |
[:text_prompts, :load, :stop] | duration, monotonic_time | path, mode |
[:text_prompts, :load, :exception] | duration, monotonic_time | path, mode, kind, reason, stacktrace |
[:text_prompts, :save, :start] | system_time, monotonic_time | path, format |
[:text_prompts, :save, :stop] | duration, monotonic_time | path, format |
[:text_prompts, :save, :exception] | duration, monotonic_time | path, format, kind, reason, stacktrace |
:telemetry.attach(
"tp-load-logger",
[:text_prompts, :load, :stop],
fn _event, %{duration: d}, %{path: p}, _ ->
IO.puts("loaded #{p} in #{System.convert_time_unit(d, :native, :microsecond)}µs")
end,
nil
)CLI and Mix tasks
Build the escript:
mix escript.build
./textprompts show prompts/greet.md
./textprompts validate prompts/*.md --mode strict
./textprompts list prompts --jsonMix tasks (no escript build):
mix textprompts.show prompts/greet.md
mix textprompts.list "prompts/**/*.md"
mix textprompts.validate "prompts/**/*.md"
Global flags: --mode strict|allow|ignore, --json (requires :jason).
Examples
Runnable scripts under examples/ create their own fixtures in tmp/:
cd packages/textprompts-ex
mix run examples/basic_load.exs
mix run examples/format_with_placeholders.exs
mix run examples/sections_toc.exs
mix run examples/round_trip.exs
Or from the repo root: make ex-test-examples.
Development
cd packages/textprompts-ex
mix deps.get
mix test
mix docs
mix format
Pre-release gate (matches CI): make ex-check.
Documentation
- HexDocs: https://hexdocs.pm/textprompts
- CHANGELOG
-
Cross-language scope:
docs/elixir-port-scope.md
License
MIT - see LICENSE.