textprompts

Hex.pmHexDocsLicense: MIT

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: :allow

Quickstart

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_timepath, mode
[:text_prompts, :load, :stop]duration, monotonic_timepath, mode
[:text_prompts, :load, :exception]duration, monotonic_timepath, mode, kind, reason, stacktrace
[:text_prompts, :save, :start]system_time, monotonic_timepath, format
[:text_prompts, :save, :stop]duration, monotonic_timepath, format
[:text_prompts, :save, :exception]duration, monotonic_timepath, 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 --json

Mix 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

License

MIT - see LICENSE.