Sign in

    uri_template

    RFC 6570 Level 4 URI Template parser and expander

    uri
    template
    rfc6570
    Download zip
    Author
    Version
    0.5.1
    License
    Apache-2.0
    Last updated
    yesterday
    Downloads
    63

    No dependencies

    #MoonBit URI Template

    RFC 6570 Level 4 URI 模板库。核心用 MoonBit 编写,不依赖任何第三方库,可运行于 JS、wasm、wasm-gc 和 Native;同一份核心还提供 Node/TypeScript SDK、命令行和浏览器工作台。

    /users/{id}{?page,tags*} + id=42, page=2, tags=[sdk, api] → /users/42?page=2&tags=sdk&tags=api

    版本:0.5.0,已发布到 Mooncakes(cjyhjy/uri_template)和 npm(moonbit-uri-template,带来源证明)。

    #为什么用它

    • 展开结果和主流实现一致。 除 270 条上游标准用例外,另有 644 条用例与 Python uritemplate、std-uritemplate 逐字节比对,其中 583 条三方完全一致,其余分歧逐条说明理由(见 一致性)。前缀截断按原始码点计数,与两个参考实现一致;另外两个 MoonBit 同类库在这里都与参考实现不同。
    • 发请求前发现配置错误。 可选择性声明必填变量和值类型(标量/列表/关联),并检查无用绑定和输入/输出预算;出错时给出模板位置、JSON 文件行列和拼写修复建议。
    • 按 JS 开发者习惯工作。 数字和布尔值会自动转成字符串,undefined 视为未绑定,对象成员保持插入顺序;TypeScript 能从字面量模板推导变量名并检查必填项。
    • 直接使用 OpenAPI 描述。 OpenAPI 用 RFC 6570 定义参数怎样进入 URL。本项目把每个接口的路径和 query 参数转换成一个 URI 模板,按接口名生成请求地址;分析在 MoonBit 中实现,Mooncakes 和 npm 用户用的是同一份代码;GitHub 官方描述的 1242 个接口全部转换并检查用时 0.6 秒,查出 2 处违反规范的路径和 2 处与声明不符的示例。另有 200 份公开描述(12,963 个接口)组成的回归集(OpenAPI)。
    • MoonBit 程序可以直接用。 原生 GitHub 客户端 从 Mooncakes 安装本库,读入 GitHub 官方描述,按 operationId 生成地址,用 moonbitlang/async 发出真实请求。
    • 快。 固定负载下,与两个同类库同机对比,全部场景的耗时中位数最低(性能)。

    #Node / TypeScript

    // npm install moonbit-uri-template import { compileTemplate } from 'moonbit-uri-template'; const user = compileTemplate('/users/{id}{?page}', { required: ['id'] }); user.expand({ id: 42 }); // /users/42 user.expand({ id: 42, page: 2 }); // /users/42?page=2 user.expandMany([{ id: 1 }, { id: 2 }]); // 任一失败则整批抛错,不返回半批结果

    目录、批量、预检、升级比较和增量链接会话见 SDK 指南。

    #MoonBit

    // 下游 moon.pkg import { "cjyhjy/uri_template" @uri }

    let vars = @uri.Variables::from_entries([
    ("id", @uri.Scalar("42")),
    ("page", @uri.Scalar("2")),
    ]).unwrap()
    let template = @uri.parse("/users/{id}{?page}").unwrap()
    println(template.expand(vars).unwrap()) // /users/42?page=2
    // 有界并检查必填:
    template.expand(vars, required=["id"], limits=@uri.ExpansionLimits::new())

    安装:moon add cjyhjy/uri_template@0.5.0。全部接口见 API 与 接口快照。从 OpenAPI 描述生成模板用 cjyhjy/uri_template/openapi 包,见 OpenAPI。

    #命令行

    node tools/cli.mjs expand '/search{?q,tags*}' --vars '{"q":"moon bit","tags":["sdk","api"]}' # /search?q=moon%20bit&tags=sdk&tags=api node tools/cli.mjs check '/users/{id}{?page}' --vars '{"paeg":2}' # 报告 missing: id, page;unused: paeg;退出码 1 node tools/cli.mjs validate '/users/{id' # UnclosedExpression at 1:8 (code points 7..10): expected closing brace # /users/{id # ^^^ node tools/cli.mjs project integrations/github-audit/requests.json --github # 一次检查整个请求配置,输出 GitHub Actions 注解 node tools/cli.mjs openapi api.github.com.json # api.github.com.json:29475:50 EquivalentPaths: same path hierarchy as /orgs/{org}/attestations/{attestation_id}

    命令:expand、measure、trace、check、validate、variables、analyze、compare、project、openapi。退出码 0 成功,1 模板或检查问题,2 参数/文件/JSON 错误。

    #从源码开始

    需要 Node.js 22+ 和 Python 3.9+。

    python3 tools/bootstrap.py # 在 .toolchain/ 内安装固定版本的 MoonBit 工具链 npm ci --ignore-scripts npm run build npm test # 四后端测试、SDK、CLI、差分回放、打包与独立消费者 npm run playground # 浏览器工作台 http://127.0.0.1:4173

    #文档

    文档内容
    评审指南与同类库的差异、证据和复现命令
    SDKNode/TypeScript 接入
    APIMoonBit 公共接口
    一致性RFC 语义选择、上游用例与差分结果
    同类对比与 Magic486、JSJ222 的功能和行为对比
    OpenAPI从 OpenAPI 描述生成模板、检查与公开文档实测
    性能基准方法与结果
    架构模块划分与测试策略
    发布打包、发布与工具链维护
    MoonBit 原生客户端从 Mooncakes 安装、按 OpenAPI 发真实请求的 MoonBit 程序
    GitHub 接入案例Node 下游客户端:请求配置检查、升级审查
    公共 API 客户端npm、Open Library 只读客户端
    AI 辅助说明开发方式

    #范围

    库本身只生成 URI 引用:不发 HTTP 请求(integrations/ 中的客户端负责发请求),不做 URL 规范化、反向路由匹配或 Unicode 归一化。模板字面量默认接受 ASCII 单引号以兼容上游用例,parse_strict / strict: true 按 RFC 语法拒绝。

    #来源与许可证

    Apache-2.0。依据 RFC 6570 独立实现,未复制其他实现的代码。测试用例来自 uri-templates/uritemplate-test,保留其许可证。详见 NOTICE。

    BatchError

    pub(all) struct BatchError {
    index : Int
    error : TemplateError
    } derive(
    Debug
    )

    index=-1 denotes a batch-level error before visiting any item.

    BatchLimits

    pub(all) struct BatchLimits {
    max_items : Int
    max_total_input_codepoints : Int
    max_total_output_bytes : Int
    } derive(
    Debug
    )

    BatchLimits::new

    fn BatchLimits::new(max_items? : Int, max_total_input_codepoints? : Int, max_total_output_bytes? : Int) -> BatchLimits

    BindingChange

    pub(all) enum BindingChange {
    SetBinding(String, Value)
    RemoveBinding(String)
    }

    BindingFinding

    pub(all) struct BindingFinding {
    code : String
    variable : String
    span : Span?
    } derive(
    Debug
    )

    BindingIssue

    pub(all) struct BindingIssue {
    name : String
    span : Span
    } derive(
    Debug
    )

    BindingKind

    pub(all) enum BindingKind {
    ScalarKind
    ListKind
    AssocKind
    } derive(Eq,
    Debug
    )

    Application value shapes; formatting numbers/booleans remains the caller's job.

    BindingKind::equal

    fn BindingKind::equal(BindingKind, BindingKind) -> Bool

    BindingKind::not_equal

    fn BindingKind::not_equal(x : BindingKind, y : BindingKind) -> Bool

    BindingReport

    pub(all) struct BindingReport {
    referenced : Array[String]
    missing : Array[BindingIssue]
    omitted : Array[BindingIssue]
    unused : Array[String]
    invalid : Array[TemplateError]
    } derive(
    Debug
    )

    Missing/omitted entries use first occurrence order; unused names are sorted.

    BindingRule

    pub(all) struct BindingRule {
    name : String
    kind : BindingKind
    } derive(
    Debug
    )

    BoundTemplate

    type BoundTemplate

    Prepared partial binding keeps original operators intact. Fixed names cannot be overridden; input collections are copied. It is not residual-template text.

    BoundTemplate::expand

    fn BoundTemplate::expand(self : BoundTemplate, variables : Variables, required? : Array[String], limits? : ExpansionLimits) -> Result[String, TemplateError]

    BoundTemplate::measure

    fn BoundTemplate::measure(self : BoundTemplate, variables : Variables, required? : Array[String], limits? : ExpansionLimits) -> Result[Int, TemplateError]

    CatalogAudit

    pub(all) struct CatalogAudit {
    entries : Array[CatalogEntryAudit]
    total_findings : Int
    } derive(
    Debug
    )

    CatalogAudit::is_clean

    fn CatalogAudit::is_clean(self : CatalogAudit) -> Bool

    CatalogBatchError

    pub(all) struct CatalogBatchError {
    index : Int
    name : String
    error : TemplateError
    } derive(
    Debug
    )

    index=-1 means a batch option/count error; otherwise identifies the request.

    CatalogContractDiff

    pub(all) struct CatalogContractDiff {
    added : Array[String]
    removed : Array[String]
    changed : Array[(String, DetailedContractDiff)]
    } derive(
    Debug
    )

    CatalogEntryAudit

    pub(all) struct CatalogEntryAudit {
    name : String
    expression_count : Int
    variable_count : Int
    max_level : Int
    findings : Array[TemplateError]
    } derive(
    Debug
    )

    CatalogError

    pub(all) struct CatalogError {
    name : String
    error : TemplateError
    } derive(
    Debug
    )

    CatalogRequest

    pub(all) struct CatalogRequest {
    name : String
    variables : Variables
    required : Array[String]
    } derive(
    Debug
    )

    One request in a heterogeneous catalog batch.

    CatalogRequest::new

    fn CatalogRequest::new(name : String, variables : Variables, required? : Array[String]) -> CatalogRequest

    ContractTemplate

    type ContractTemplate

    Captures rules independently of caller-owned arrays. No bound values retained.

    ContractTemplate::expand

    fn ContractTemplate::expand(self : ContractTemplate, variables : Variables, limits? : ExpansionLimits) -> Result[String, TemplateError]

    ContractTemplate::expand_mapped

    fn ContractTemplate::expand_mapped(self : ContractTemplate, variables : Variables, limits? : ExpansionLimits) -> Result[Expansion, TemplateError]

    ContractTemplate::inspect

    fn ContractTemplate::inspect(self : ContractTemplate, variables : Variables, limits? : ExpansionLimits, max_findings? : Int) -> Result[Array[BindingFinding], TemplateError]

    Collect presence, unused and shape findings without exposing bound values. The cap applies to the complete report; overflow never returns a clean subset.

    ContractTemplate::measure

    fn ContractTemplate::measure(self : ContractTemplate, variables : Variables, limits? : ExpansionLimits) -> Result[Int, TemplateError]

    ContractTemplate::validate

    fn ContractTemplate::validate(self : ContractTemplate, variables : Variables, limits? : ExpansionLimits) -> Result[Unit, TemplateError]

    Input/shape/presence validation only; output limits are enforced by measure.

    DetailedContractDiff

    pub(all) struct DetailedContractDiff {
    added_variables : Array[String]
    removed_variables : Array[String]
    became_scalar_only : Array[String]
    became_composite_capable : Array[String]
    new_repeated_variables : Array[String]
    old_level : Int
    new_level : Int
    reserved_added : Bool
    fragment_added : Bool
    literals_changed : Bool
    expression_layout_changed : Bool
    variable_changes : Array[VariableChange]
    source_changed : Bool
    } derive(
    Debug
    )

    A review aid, never a claim of business or URL equivalence.

    DetailedContractDiff::requires_review

    fn DetailedContractDiff::requires_review(self : DetailedContractDiff) -> Bool

    Expansion

    pub(all) struct Expansion {
    output : String
    bytes : Int
    origins : Array[OutputOrigin]
    } derive(
    Debug
    )

    ExpansionLimits

    pub(all) struct ExpansionLimits {
    max_bindings : Int
    max_members : Int
    max_value_codepoints : Int
    max_total_value_codepoints : Int
    max_output_bytes : Int
    } derive(
    Debug
    )

    ExpansionLimits::new

    fn ExpansionLimits::new(max_bindings? : Int, max_members? : Int, max_value_codepoints? : Int, max_total_value_codepoints? : Int, max_output_bytes? : Int) -> ExpansionLimits

    ExpressionInfo

    pub(all) struct ExpressionInfo {
    operator : String
    level : Int
    span : Span
    variables : Array[String]
    } derive(
    Debug
    )

    LinkChange

    pub(all) struct LinkChange {
    name : String
    before : String
    after : String
    } derive(
    Debug
    )

    LinkSession

    type LinkSession

    Immutable captured bindings and URI outputs; methods return detached arrays.

    LinkSession::get

    fn LinkSession::get(self : LinkSession, name : String) -> Result[String, CatalogError]

    fn LinkSession::links(self : LinkSession) -> Array[(String, String)]

    LinkSession::total_bytes

    fn LinkSession::total_bytes(self : LinkSession) -> Int

    LinkSession::update

    fn LinkSession::update(self : LinkSession, changes : Array[BindingChange]) -> Result[LinkUpdate, CatalogError]

    Stage a patch, validate the complete next environment and total output, then render only affected endpoints. Neither failure nor success mutates self.

    LinkUpdate

    type LinkUpdate

    LinkUpdate::changes

    fn LinkUpdate::changes(self : LinkUpdate) -> Array[LinkChange]

    LinkUpdate::recomputed

    fn LinkUpdate::recomputed(self : LinkUpdate) -> Int

    LinkUpdate::session

    fn LinkUpdate::session(self : LinkUpdate) -> LinkSession

    OrderedEntries

    type OrderedEntries derive(
    Debug
    )

    An ordered, unique-key associative value. Construct with from_entries.

    OrderedEntries::from_entries

    fn OrderedEntries::from_entries(entries : Array[(String, String)]) -> Result[OrderedEntries, TemplateError]

    OutputOrigin

    pub(all) struct OutputOrigin {
    name : String?
    source : Span
    output_start : Int
    output_end : Int
    } derive(
    Debug
    )

    Output byte ranges include the punctuation emitted for that variable. One entry per literal or variable occurrence, not per collection member.

    ParseLimits

    pub(all) struct ParseLimits {
    max_template_codepoints : Int
    max_expressions : Int
    max_variables_per_expression : Int
    max_total_variables : Int
    } derive(
    Debug
    )

    Limits count Unicode code points; output is ASCII URI bytes. Zero is allowed.

    ParseLimits::new

    fn ParseLimits::new(max_template_codepoints? : Int, max_expressions? : Int, max_variables_per_expression? : Int, max_total_variables? : Int) -> ParseLimits

    RequestAudit

    pub(all) struct RequestAudit {
    requests : Array[RequestInspection]
    total_findings : Int
    } derive(
    Debug
    )

    RequestAudit::is_clean

    fn RequestAudit::is_clean(self : RequestAudit) -> Bool

    RequestInspection

    pub(all) struct RequestInspection {
    index : Int
    name : String
    findings : Array[BindingFinding]
    } derive(
    Debug
    )

    Span

    pub(all) struct Span {
    start : Int
    end : Int
    } derive(
    Debug
    )

    Source range in Unicode code points, zero-based and half-open.

    Span::to_repr

    Template

    A validated template with a hidden, immutable representation.

    Template::analyze

    fn Template::analyze(self : Template) -> TemplateAnalysis

    Template::audit

    fn Template::audit(self : Template, policy : TemplatePolicy, max_findings? : Int) -> Result[Array[TemplateError], TemplateError]

    Static policy findings, at most max_findings of them.

    Template::bind

    fn Template::bind(self : Template, fixed : Variables, limits? : ExpansionLimits) -> Result[BoundTemplate, TemplateError]

    Template::check_bindings

    fn Template::check_bindings(self : Template, variables : Variables, limits? : ExpansionLimits) -> Result[BindingReport, TemplateError]

    Read-only preflight. Empty scalar is defined; empty collections are omitted. Reports all incompatible prefix occurrences without expanding any values.

    Template::check_contract

    fn Template::check_contract(self : Template, variables : Variables, required? : Array[String], reject_unused? : Bool, limits? : ExpansionLimits, max_findings? : Int) -> Result[Array[BindingFinding], TemplateError]

    Collect business-contract defects; optional omissions are not errors. Invalid budgets / excessive reports return Err, never an incomplete clean report.

    Template::diff

    fn Template::diff(self : Template, newer : Template) -> DetailedContractDiff

    Template::expand

    fn Template::expand(self : Template, variables : Variables, required? : Array[String], reject_unused? : Bool, limits? : ExpansionLimits) -> Result[String, TemplateError]

    Expand with RFC omission semantics. With no options the call is unbounded (the 0.1 behaviour). Passing limits, required or reject_unused checks every binding first, then streams within the budget; no partial result escapes.

    Template::expand_batch

    fn Template::expand_batch(self : Template, inputs : Array[Variables], required? : Array[String], reject_unused? : Bool, limits? : ExpansionLimits, batch_limits? : BatchLimits) -> Result[Array[String], BatchError]

    Atomic: validate and measure every item before allocating any result URI. Errors include the zero-based failing item. No partial array is returned.

    Template::expand_mapped

    fn Template::expand_mapped(self : Template, variables : Variables, required? : Array[String], reject_unused? : Bool, limits? : ExpansionLimits) -> Result[Expansion, TemplateError]

    Template::measure

    fn Template::measure(self : Template, variables : Variables, required? : Array[String], reject_unused? : Bool, limits? : ExpansionLimits) -> Result[Int, TemplateError]

    Count encoded bytes using the very same emitter, without building URI text.

    Template::source

    fn Template::source(self : Template) -> String

    Template::to_repr

    Template::variables

    fn Template::variables(self : Template) -> Array[String]

    Variable names in first-occurrence order, retaining their encoded spelling.

    Template::with_contract

    fn Template::with_contract(self : Template, rules : Array[BindingRule], required? : Array[String], reject_unused? : Bool, max_rules? : Int, max_rule_codepoints? : Int) -> Result[ContractTemplate, TemplateError]

    Compile shape constraints once, separately from RFC expansion semantics. Prefix variables always require scalars, even for empty composite bindings.

    TemplateAnalysis

    pub(all) struct TemplateAnalysis {
    expressions : Array[ExpressionInfo]
    uses : Array[VariableUse]
    profiles : Array[VariableProfile]
    max_level : Int
    has_reserved_expansion : Bool
    has_fragment : Bool
    } derive(
    Debug
    )

    TemplateCatalog

    type TemplateCatalog

    TemplateCatalog::audit

    fn TemplateCatalog::audit(self : TemplateCatalog, policy : TemplatePolicy, max_findings? : Int) -> Result[CatalogAudit, CatalogError]

    TemplateCatalog::contains

    fn TemplateCatalog::contains(self : TemplateCatalog, name : String) -> Bool

    TemplateCatalog::diff

    TemplateCatalog::expand

    fn TemplateCatalog::expand(self : TemplateCatalog, name : String, variables : Variables, required? : Array[String], reject_unused? : Bool, limits? : ExpansionLimits) -> Result[String, CatalogError]

    TemplateCatalog::expand_batch

    fn TemplateCatalog::expand_batch(self : TemplateCatalog, requests : Array[CatalogRequest], reject_unused? : Bool, limits? : ExpansionLimits, batch_limits? : BatchLimits) -> Result[Array[String], CatalogBatchError]

    Atomic multi-template expansion: preflight all requests before producing URIs.

    TemplateCatalog::get

    fn TemplateCatalog::get(self : TemplateCatalog, name : String) -> Result[Template, CatalogError]

    TemplateCatalog::inspect_requests

    fn TemplateCatalog::inspect_requests(self : TemplateCatalog, requests : Array[CatalogRequest], reject_unused? : Bool, limits? : ExpansionLimits, batch_limits? : BatchLimits, max_findings? : Int) -> Result[RequestAudit, CatalogBatchError]

    All requests are inspected; budgets fail closed with an indexed error. Output budgets belong to measure_batch / expand_batch, not binding inspection.

    TemplateCatalog::measure

    fn TemplateCatalog::measure(self : TemplateCatalog, name : String, variables : Variables, required? : Array[String], limits? : ExpansionLimits) -> Result[Int, CatalogError]

    TemplateCatalog::measure_batch

    fn TemplateCatalog::measure_batch(self : TemplateCatalog, requests : Array[CatalogRequest], reject_unused? : Bool, limits? : ExpansionLimits, batch_limits? : BatchLimits) -> Result[Array[Int], CatalogBatchError]

    Validate the entire plan without materializing any result URI. Aggregate input counts variable names/values and catalog request names.

    TemplateCatalog::names

    fn TemplateCatalog::names(self : TemplateCatalog) -> Array[String]

    TemplateCatalog::start_session

    fn TemplateCatalog::start_session(self : TemplateCatalog, variables : Variables, contracts? : Array[(String, ContractTemplate)], limits? : ExpansionLimits, max_total_output_bytes? : Int, max_patch_bindings? : Int, max_dependency_edges? : Int) -> Result[LinkSession, CatalogError]

    Contracts are optional endpoint policies and must refer to the same source. Every shared input counts once against the session's cumulative input budget.

    TemplateCatalog::templates_using

    fn TemplateCatalog::templates_using(self : TemplateCatalog, variable : String) -> Array[String]

    TemplateError

    pub(all) struct TemplateError {
    code : String
    message : String
    span : Span
    } derive(
    Debug
    )

    A stable error code and the relevant source range.

    TemplatePolicy

    pub(all) struct TemplatePolicy {
    allow_reserved : Bool
    allow_fragment : Bool
    allow_prefix : Bool
    allow_explode : Bool
    allowed_variables : Array[String]?
    max_level : Int
    max_references : Int
    forbid_repeated_variables : Bool
    } derive(
    Debug
    )

    Static policy. This does not validate a host, scheme, or prevent SSRF.

    TemplatePolicy::new

    fn TemplatePolicy::new(allow_reserved? : Bool, allow_fragment? : Bool, allow_prefix? : Bool, allow_explode? : Bool, allowed_variables? : Array[String]?, max_level? : Int, max_references? : Int, forbid_repeated_variables? : Bool) -> TemplatePolicy

    Value

    pub(all) enum Value {
    Scalar(String)
    List(Array[String])
    Assoc(OrderedEntries)
    } derive(
    Debug
    )

    Values are strings; callers choose numeric and boolean formatting explicitly.

    Value::to_repr

    VariableChange

    pub(all) struct VariableChange {
    name : String
    before : VariableProfile
    after : VariableProfile
    operators_changed : Bool
    prefixes_changed : Bool
    explode_changed : Bool
    occurrences_changed : Bool
    uses_changed : Bool
    } derive(
    Debug
    )

    VariableProfile

    pub(all) struct VariableProfile {
    name : String
    occurrences : Int
    first_span : Span
    last_span : Span
    scalar_only : Bool
    uses_explode : Bool
    uses_reserved : Bool
    uses_query : Bool
    uses_fragment : Bool
    maximum_prefix : Int?
    operators : Array[String]
    prefixes : Array[Int]
    uses : Array[VariableUse]
    } derive(
    Debug
    )

    Ordered by first occurrence. All uses are retained, not just a summary.

    VariableUse

    pub(all) struct VariableUse {
    name : String
    operator : String
    prefix : Int?
    explode : Bool
    expression_index : Int
    span : Span
    } derive(
    Debug
    )

    Variables

    type Variables derive(
    Debug
    )

    Mutable bindings. Collection values are copied when inserted.

    Variables::from_entries

    fn Variables::from_entries(entries : Array[(String, Value)]) -> Result[Variables, TemplateError]

    Construct a copied binding set, rejecting ambiguous duplicate names.

    Variables::get

    fn Variables::get(self : Variables, name : String) -> Value?

    Return a captured value so callers cannot mutate stored collections.

    Variables::names

    fn Variables::names(self : Variables) -> Array[String]

    Binding names in insertion order.

    Variables::new

    fn Variables::new() -> Variables

    Variables::remove

    fn Variables::remove(self : Variables, name : String) -> Unit

    Variables::set

    fn Variables::set(self : Variables, name : String, value : Value) -> Unit

    compile_catalog

    fn compile_catalog(entries : Array[(String, String)], max_templates? : Int, max_total_codepoints? : Int, limits? : ParseLimits, strict? : Bool) -> Result[TemplateCatalog, CatalogError]

    Compile a bounded directory once; later lookup reuses parsed templates.

    expand

    fn expand(source : String, variables : Variables) -> Result[String, TemplateError]

    parse

    fn parse(source : String) -> Result[Template, TemplateError]

    Parse a template with the documented single-quote literal compatibility extension.

    parse_strict

    fn parse_strict(source : String) -> Result[Template, TemplateError]

    Parse using RFC 6570 literal ABNF, rejecting literal ASCII apostrophes. Variable values are still governed by the operator's encoding rules.

    parse_with_limits

    fn parse_with_limits(source : String, limits : ParseLimits, strict? : Bool) -> Result[Template, TemplateError]

    Bounded parsing; checks source size before allocating its character array.

    sparse_assoc

    fn sparse_assoc(items : Array[(String, String?)]) -> Result[Value, TemplateError]

    sparse_list

    fn sparse_list(items : Array[String?]) -> Value

    Normalize sparse entries before binding; null list members are omitted here. JSON adapter remains strict for list nulls to catch accidental input errors.