moon-weblink

RFC 8288 Web Linking and RFC 9264 Linkset parser, serializer, query and audit toolkit for MoonBit.

web-linking
linkset
rfc8288
rfc9264
rfc8187
http
link
iana
moonbit
moon add 15614376790/moon-weblink@0.1.0
Download zip
Version
0.1.0
License
Apache-2.0
Last updated
4 days ago
Downloads
4
README

#moon-weblink

A strict RFC 8288 Web Linking and RFC 9264 Linkset parser, serializer, converter, query and audit toolkit for MoonBit.

moon-weblink parses, serializes, converts, queries and audits link collections in all three representations RFC 8288 / RFC 9264 define — the HTTP Link header field, the application/linkset text format, and the application/linkset+json format — over one shared model. It ships an offline snapshot of the IANA Link Relation Types registry, a deterministic audit layer, bounded resource limits, and structured errors with UTF-8 byte offsets.

#Features

  • RFC 8288 Link header parsing and serialization — link-values, target attributes (anchor, hreflang, media, title, title*, type), extension parameters, first-wins duplicate handling, and a deterministic canonical form.
  • RFC 8187 extended valuestitle* and name* parameters, full charset/language/percent-decoding, plus a pure function serializer.
  • RFC 3986 URI-reference support — a minimal but strict parser and reference resolution (Section 5.2.2), used for targets, anchors and resolving relative links.
  • RFC 9264 Linkset — both application/linkset (multiline text) and application/linkset+json, with semantics-preserving conversion at the model level between the header, text and JSON forms for the supported RFC 8288 / RFC 9264 scope.
  • Offline IANA registry — 134 relation types bundled as a generated snapshot (see testdata/iana/); membership checks never touch the network.
  • Query API — find and filter links by relation type, media type and hreflang; next / prev / canonical / alternate helpers.
  • Deterministic audit — flags the deprecated rev, relation types that look registered but are not, title/title* coexistence, duplicates, relative anchors and out-of-limits values.
  • Resource limitsdefault, strict and permissive presets bounded by input size, link count, parameter count, name/value sizes and more.
  • Structured errorsLinkError with a stable (stage, kind) pair, a UTF-8 byte offset into the input, and a context excerpt capped at 80 bytes.
  • CLIweblink-tool subcommands to parse, validate, canonicalize, query, convert, look up relations and audit.
  • Safety — truncation-safe (every byte-prefix of any input never panics) and malformed-input-safe; 147 named tests pass on all three targets.

#Layout

moon.mod module manifest (15614376790/moon-weblink 0.1.0) lib source (.mbt) parser, serializer, model, linkset, audit, query, ... cmd/weblink-tool/ command-line tool examples/ five runnable example programs testdata/iana/ offline IANA registry snapshot (CSV + SOURCE.json) scripts/ verification and generation helpers docs/ design and process documentation

#Quick start

Requires the MoonBit toolchain. From the repository root:

moon check --target native moon test --target native moon run cmd/weblink-tool -- parse --input '<https://example.com/page/2>; rel="next"' moon run examples/pagination

Verify everything (formatting, all three targets, CLI, examples, line budgets, IANA snapshot):

powershell -ExecutionPolicy Bypass -File scripts\verify_all.ps1

#Library usage

fn main {
// Parse a Link header field value into a shared model.
let links = @weblink.parse_link_header(
"</users?page=2>; rel=\"next\", </users?page=1>; rel=\"prev\"",
@weblink.Limits::default(),
)

match links {
Err(e) => println("error: \{e.to_display()}")
Ok(links) => {
// Query helpers: find next/prev, filter by relation or media type.
match @weblink.find_next(links) {
Some(next) => println("next: \{next.target()}")
None => println("no next link")
}
// Convert the whole collection to application/linkset+json and back.
let json = @weblink.serialize_linkset_json(
@weblink.LinkSet::from_links(links),
)
println(json)
}
}
}

#CLI

The CLI reads its input from --input (or the first positional). There is no stdin or file I/O in this MoonBit core, and fn main must return Unit (there is no portable exit code), so every command reports its outcome as one deterministic line of stdout text, safe to script by grepping.

weblink-tool parse parse a Link header and print each link weblink-tool validate report valid / invalid weblink-tool canonicalize emit the canonical Link header form weblink-tool query find links by --rel / --type / --hreflang weblink-tool to-linkset-json convert a Link header to application/linkset+json weblink-tool from-linkset-json convert application/linkset+json to a Link header weblink-tool to-linkset-text convert to application/linkset text weblink-tool relation query the offline IANA relation registry weblink-tool audit audit a Link header for issues weblink-tool stats library and registry statistics weblink-tool version | help

--limits selects the default, strict or permissive resource limit preset; --json switches the parse and audit output to machine-readable JSON.

#Examples

ExampleWhat it shows
examples/parse_headerparse a Link header and print each link
examples/paginationwalk next / prev pagination links; resolve a relative URI against a base
examples/linkset_jsonconvert Link header → linkset JSON → header
examples/relation_queryquery the offline IANA registry; filter links by relation
examples/audit_headerrun the deterministic audit and print every finding

#Documentation

  • Architecture — modules, data model, error model, determinism.
  • Specification map — every RFC requirement and where it is implemented.
  • Testing — the 147 tests, property and truncation corpora, targets.
  • Reproduction — how to reproduce every number in this README.
  • Security — parser safety, resource limits, no network, no secrets.
  • Limitations — what this toolkit deliberately does not do.
  • Renaming — record of the completed localdev/moon-weblink15614376790/moon-weblink namespace rename, and how to rename again.
  • CHANGELOG — release history.
  • CONTRIBUTING — how to build, test and contribute.
  • THIRD_PARTY_NOTICES — IANA registry data provenance.

#Measured numbers

Reproduced by scripts/count_code.py and scripts/verify_iana_snapshot.py:

  • 147 named tests (140 library blackbox tests + 7 CLI argument-handling tests), all passing on wasm-gc, js and native.
  • 1200 deterministic property-test cases (fixed seeds) + 2504 truncation (prefix, parser) cases — every byte-prefix of the complex inputs parses without panicking.
  • Code lines (blank and comment lines excluded): core 3403, CLI + examples 872, tests 1958, total 6233.
  • 134 relation types in the offline IANA snapshot; the checked-in generated_relations.mbt matches the snapshot byte-for-byte.

#License

Apache-2.0. The IANA registry data embedded in this project is from the IANA Link Relation Types registry; see THIRD_PARTY_NOTICES.

#
LinkError

pub(all) suberror LinkError {
LinkError(LinkErrorStage, LinkErrorKind, Int, String)
}

A structured error returned by every public API.

  • stage() — where the failure happened.
  • kind() — what failed.
  • offset() — UTF-8 byte offset into the input, 0 when not meaningful.
  • context() — short excerpt of the input at the failure point (bounded, never the full input).

#
LinkError::context

fn LinkError::context(self : LinkError) -> String

A short, bounded excerpt of the input around the failure point.

#
LinkError::kind

fn LinkError::kind(self : LinkError) -> LinkErrorKind

The concrete kind of this error.

#
LinkError::offset

fn LinkError::offset(self : LinkError) -> Int

The UTF-8 byte offset into the input where the error was detected, or 0 when the offset is not meaningful for this error kind.

#
LinkError::stage

fn LinkError::stage(self : LinkError) -> LinkErrorStage

The stage in which this error was detected.

#
LinkError::to_display

fn LinkError::to_display(self : LinkError) -> String

A single-line human readable rendering of the error, intended for terminal output and CLI use.

#
AuditIssue

pub struct AuditIssue {
severity : AuditSeverity
code : String
message : String
link_index : Int
}

A single audit finding. code is a stable, machine-readable identifier; message is human readable; link_index is the 0-based index of the offending link, or -1 for a set-level finding.

#
AuditIssue::code

fn AuditIssue::code(self : AuditIssue) -> String

The stable machine-readable code of this finding.
fn AuditIssue::link_index(self : AuditIssue) -> Int

The 0-based index of the offending link, or -1 for set-level findings.

#
AuditIssue::message

fn AuditIssue::message(self : AuditIssue) -> String

A human readable description of this finding.

#
AuditIssue::severity

fn AuditIssue::severity(self : AuditIssue) -> AuditSeverity

The severity of this finding.

#
AuditReport

pub struct AuditReport {
issues : Array[AuditIssue]
}

The result of auditing a link set: every finding, in deterministic order.

#
AuditReport::count_at

fn AuditReport::count_at(self : AuditReport, severity : AuditSeverity) -> Int

The number of findings at the given severity.

#
AuditReport::has_errors

fn AuditReport::has_errors(self : AuditReport) -> Bool

Whether the report contains at least one Error-severity finding.

#
AuditReport::is_clean

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

Whether the report contains no findings.

#
AuditReport::issue_count

fn AuditReport::issue_count(self : AuditReport) -> Int

The number of findings.

#
AuditReport::issues

fn AuditReport::issues(self : AuditReport) -> Array[AuditIssue]

The findings, in deterministic order.

#
AuditSeverity

pub enum AuditSeverity {
Info
Warning
Error
} derive(Eq)

The importance of an audit finding.

#
AuditSeverity::to_string

fn AuditSeverity::to_string(self : AuditSeverity) -> String

The stable lowercase name of a severity, used by the CLI JSON output.

#
ExtendedValue

pub struct ExtendedValue {
charset : String
language : String?
value : String
}

An RFC 8187 extended value (used by title* and name* parameters): a charset, an optional language tag, and the decoded value.

#
ExtendedValue::charset

fn ExtendedValue::charset(self : ExtendedValue) -> String

The charset of this extended value.

#
ExtendedValue::language

fn ExtendedValue::language(self : ExtendedValue) -> String?

The optional language tag of this extended value.

#
ExtendedValue::value

fn ExtendedValue::value(self : ExtendedValue) -> String

The decoded value of this extended value.

#
Limits

pub struct Limits {
max_input_bytes : Int
max_links : Int
max_params_per_link : Int
max_relations_per_link : Int
max_target_bytes : Int
max_parameter_name_bytes : Int
max_parameter_value_bytes : Int
max_quoted_string_bytes : Int
max_linkset_links : Int
max_json_bytes : Int
}

Bounds applied while parsing link data.

#
Limits::default

fn Limits::default() -> Limits

Default limits. Intended for interactive and typical server use.

#
Limits::max_input_bytes

fn Limits::max_input_bytes(self : Limits) -> Int

Accessor for the input byte bound.

#
Limits::max_json_bytes

fn Limits::max_json_bytes(self : Limits) -> Int

Accessor for the Linkset JSON document byte bound.
fn Limits::max_links(self : Limits) -> Int

Accessor for the per-document link count bound.
fn Limits::max_linkset_links(self : Limits) -> Int

Accessor for the Linkset link count bound.

#
Limits::max_parameter_name_bytes

fn Limits::max_parameter_name_bytes(self : Limits) -> Int

Accessor for the parameter name byte bound.

#
Limits::max_parameter_value_bytes

fn Limits::max_parameter_value_bytes(self : Limits) -> Int

Accessor for the parameter value byte bound.
fn Limits::max_params_per_link(self : Limits) -> Int

Accessor for the per-link parameter count bound.

#
Limits::max_quoted_string_bytes

fn Limits::max_quoted_string_bytes(self : Limits) -> Int

Accessor for the quoted-string byte bound.
fn Limits::max_relations_per_link(self : Limits) -> Int

Accessor for the per-link relation count bound.

#
Limits::max_target_bytes

fn Limits::max_target_bytes(self : Limits) -> Int

Accessor for the target (URI-reference) byte bound.

#
Limits::permissive

fn Limits::permissive() -> Limits

Permissive limits. Intended for batch processing of trusted data with very large link sets.

#
Limits::strict

fn Limits::strict() -> Limits

Strict limits. Intended for constrained deployments where every byte counts and where the caller expects small, well-formed link data.

#
LinkErrorKind

pub(all) enum LinkErrorKind {
EmptyInput
UnexpectedCharacter
ExpectedAngleBracket
UnterminatedTarget
InvalidTarget
InvalidToken
MissingParameterName
InvalidParameter
UnterminatedQuotedString
InvalidQuotedPair
InvalidRelation
InvalidExtensionRelation
InvalidPercentEncoding
UnsupportedCharset
InvalidUtf8
InvalidLanguageTag
InvalidJson
InvalidJsonShape
InvalidLinkset
DuplicateParameter
LimitExceeded
TrailingInput
InvalidContextValue
InvalidMediaType
} derive(Eq)

The concrete error category. Stable across versions so callers can switch on it without string matching.

#
LinkErrorKind::to_string

fn LinkErrorKind::to_string(self : LinkErrorKind) -> String

Stable programmatic name for a kind, used by the CLI JSON output.

#
LinkErrorStage

pub(all) enum LinkErrorStage {
Input
Header
LinkValue
Target
Parameter
Relation
QuotedString
ExtendedValue
UriReference
LinksetText
LinksetJson
Registry
Limit
} derive(Eq)

The processing stage in which an error was detected.

#
LinkErrorStage::to_string

fn LinkErrorStage::to_string(self : LinkErrorStage) -> String

Stable programmatic name for a stage, used by the CLI JSON output.

#
LinkHeaderParse

pub struct LinkHeaderParse {
links : Array[WebLink]
duplicates : Array[String]
}

The result of parsing a full Link header field value.

#
LinkHeaderParse::duplicates

fn LinkHeaderParse::duplicates(self : LinkHeaderParse) -> Array[String]

The names of the single-occurrence parameters that appeared more than once and whose later occurrences were ignored, in first-seen order.

The parsed links, in input order.

#
LinkParameter

pub struct LinkParameter {
name : String
value : String?
quoted : Bool
}

A single link parameter. value is the unquoted content; quoted records whether the original serialisation used the quoted-string form (used to preserve round-trip fidelity for extension parameters).

#
LinkParameter::name

fn LinkParameter::name(self : LinkParameter) -> String

The parameter name.

#
LinkParameter::quoted

fn LinkParameter::quoted(self : LinkParameter) -> Bool

Whether the original serialisation used the quoted-string form.

#
LinkParameter::value

fn LinkParameter::value(self : LinkParameter) -> String?

The parameter value (unquoted), if present.

#
LinkSet

pub struct LinkSet {
links : Array[WebLink]
}

A set of links. This is the model shared by the HTTP Link header field, the application/linkset text format and the application/linkset+json format.

#
LinkSet::add

fn LinkSet::add(self : LinkSet, link : WebLink) -> Unit

Appends a link to the set.
fn LinkSet::from_links(links : Array[WebLink]) -> LinkSet

Constructs a LinkSet holding the given links, in order.

#
LinkSet::is_empty

fn LinkSet::is_empty(self : LinkSet) -> Bool

Whether the set contains no links.
fn LinkSet::link_count(self : LinkSet) -> Int

The number of links in this set.
fn LinkSet::links(self : LinkSet) -> Array[WebLink]

The links in this set, in order.

#
LinkSet::new

fn LinkSet::new() -> LinkSet

Constructs an empty LinkSet.

#
ParsedParameter

pub struct ParsedParameter {
name : String
value_start : Int
value : RawParamValue
}

One parsed link-parameter: its name, the value form, and the byte offset of the first byte of the value (used for error reporting).

#
ParsedParameter::name

fn ParsedParameter::name(self : ParsedParameter) -> String

The parameter name.

#
ParsedParameter::value

The raw value form.

#
ParsedParameter::value_start

fn ParsedParameter::value_start(self : ParsedParameter) -> Int

The byte offset of the first byte of the value.

#
RawParamValue

pub enum RawParamValue {
Empty
Token(String)
Quoted(String)
Extended(ExtendedValue)
}

The raw form of a parsed parameter value, before it is dispatched to a field of the link model.

#
RawParamValue::as_string

fn RawParamValue::as_string(self : RawParamValue) -> String?

The value as a plain string: the unquoted content for the token and quoted forms, or None for flag parameters and name* parameters (which are not plain strings).

#
RawParamValue::has_value

fn RawParamValue::has_value(self : RawParamValue) -> Bool

Whether the parameter has a value at all.

#
RelationInfo

pub struct RelationInfo {
name : String
description : String
reference : String
notes : String
}

One entry of the IANA Link Relation Types registry.

#
RelationInfo::description

fn RelationInfo::description(self : RelationInfo) -> String

The registered description of the relation type.

#
RelationInfo::name

fn RelationInfo::name(self : RelationInfo) -> String

The relation type name (always lowercase in the registry).

#
RelationInfo::notes

fn RelationInfo::notes(self : RelationInfo) -> String

Additional IANA notes, if any (the empty string when absent).

#
RelationInfo::reference

fn RelationInfo::reference(self : RelationInfo) -> String

The IANA-recorded reference (an RFC, a specification, or a URI).

#
RelationType

pub enum RelationType {
Registered(String)
Extension(String)
}

The relation type of a link. Registered is the token form; Extension is an absolute URI.

#
RelationType::extension_uri

fn RelationType::extension_uri(self : RelationType) -> String?

The extension URI of an extension relation type.

#
RelationType::is_extension

fn RelationType::is_extension(self : RelationType) -> Bool

Whether this is the extension (absolute URI) form.

#
RelationType::is_registered

fn RelationType::is_registered(self : RelationType) -> Bool

Whether this is the registered (token) form.

#
RelationType::matches

fn RelationType::matches(self : RelationType, name : String) -> Bool

Whether this relation type matches the given name or URI.

Registered relation types are compared case-insensitively (RFC 8288 Section 2.1.1); extension relation types are compared as exact strings (RFC 8288 Section 2.1.2).

#
RelationType::name

fn RelationType::name(self : RelationType) -> String?

The relation name of a registered (token) relation type.

#
Scanner

pub struct Scanner {
bytes : Bytes
len : Int
pos : Int
allow_newline : Bool
}

A bounds-checked byte cursor over an input string.

#
Scanner::byte_at

fn Scanner::byte_at(self : Scanner, idx : Int) -> Byte?

The byte at an absolute offset, or None when out of bounds.

#
Scanner::consume_char

fn Scanner::consume_char(self : Scanner, b : Byte) -> Bool

If the current byte equals b, advances and returns true.

#
Scanner::consume_param_name

fn Scanner::consume_param_name(self : Scanner) -> (Int, Int)

Consumes a run of parameter characters (RFC 8288 parmchar) and returns the (start, end) byte range. The run may be empty.

#
Scanner::consume_token

fn Scanner::consume_token(self : Scanner) -> (Int, Int)

Consumes a run of token characters (RFC 7230 tchar) and returns the (start, end) byte range. The run may be empty.

#
Scanner::consume_until

fn Scanner::consume_until(self : Scanner, b : Byte) -> (Int, Int)?

Consumes bytes until (and including) the first occurrence of byte b. Returns the (start, end) range of the consumed bytes before b (i.e. end is the position of b), or None when b is not found.

#
Scanner::consume_while

fn Scanner::consume_while(self : Scanner, pred : (Byte) -> Bool) -> (Int, Int)

Consumes a run of bytes satisfying pred and returns the (start, end) byte range of the run. The run may be empty.

#
Scanner::context_string

fn Scanner::context_string(self : Scanner) -> String

A short, bounded excerpt of the input around the current position, for use in error context strings. Never longer than max_context_bytes().

#
Scanner::eof

fn Scanner::eof(self : Scanner) -> Bool

Whether the cursor is at (or past) the end of the input.

#
Scanner::find_byte

fn Scanner::find_byte(self : Scanner, b : Byte) -> Int?

The index of the next occurrence of byte b at or after the current position, or None.

#
Scanner::is_ows

fn Scanner::is_ows(self : Scanner) -> Bool

Whether the current byte is optional whitespace under this scanner's grammar.

#
Scanner::new

fn Scanner::new(input : String) -> Scanner

Creates a scanner for the RFC 8288 Link header field grammar. Optional whitespace is SP / HTAB only.

#
Scanner::new_linkset

fn Scanner::new_linkset(input : String) -> Scanner

Creates a scanner for the application/linkset text grammar, where newline characters are also permitted as whitespace around the comma separators (RFC 9264 Section 4.1). The input is scanned as raw bytes; non-ASCII bytes are rejected by the linkset parser, not by the scanner.

#
Scanner::next_byte

fn Scanner::next_byte(self : Scanner) -> Byte?

Returns the current byte and advances the cursor by one. Returns None at end of input (and does not advance).

#
Scanner::peek_at

fn Scanner::peek_at(self : Scanner, rel : Int) -> Byte?

The byte at pos + rel, or None when out of bounds. rel may be negative to look behind the current position.

#
Scanner::peek_byte

fn Scanner::peek_byte(self : Scanner) -> Byte?

The byte at the current position, or None at end of input.

#
Scanner::position

fn Scanner::position(self : Scanner) -> Int

The current position, as a UTF-8 byte offset.

#
Scanner::remaining

fn Scanner::remaining(self : Scanner) -> Int

The number of bytes remaining from the current position.

#
Scanner::seek

fn Scanner::seek(self : Scanner, pos : Int) -> Unit

Moves the cursor to an absolute byte offset, clamping out-of-range positions to the ends of the input. Used when a sub-parser must start at a known offset (for example media-type parameters).

#
Scanner::skip_ows

fn Scanner::skip_ows(self : Scanner) -> Unit

Skips optional whitespace (OWS). For the default grammar this is SP (0x20) and HTAB (0x09); for the linkset grammar CR and LF are also treated as whitespace (RFC 9264 Section 4.1).

#
Scanner::take_string

fn Scanner::take_string(self : Scanner, start : Int, end : Int) -> String

Decodes the byte range [start, end) back into a String. Valid UTF-8 is decoded normally; a range that is not valid UTF-8 (for example an HTTP obs-text byte inside a quoted-string) is preserved byte-for-byte so round-tripping never loses data.

#
Scanner::total_bytes

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

The total length of the input in bytes.

#
UriReference

pub struct UriReference {
scheme : String?
authority : String?
path : String
query : String?
fragment : String?
}

A decomposed URI-reference.

#
UriReference::authority

fn UriReference::authority(self : UriReference) -> String?

The authority component, if present.

#
UriReference::empty

fn UriReference::empty() -> UriReference

An empty URI-reference ("").

#
UriReference::fragment

fn UriReference::fragment(self : UriReference) -> String?

The fragment component (without the leading #), if present.

#
UriReference::path

fn UriReference::path(self : UriReference) -> String

The path component (possibly empty).

#
UriReference::query

fn UriReference::query(self : UriReference) -> String?

The query component (without the leading ?), if present.

#
UriReference::scheme

fn UriReference::scheme(self : UriReference) -> String?

The scheme component, if present.

#
UriReference::to_string

fn UriReference::to_string(self : UriReference) -> String

Recomposes the URI-reference from its components.
pub struct WebLink {
target : String
relations : Array[RelationType]
anchor : String?
hreflang : Array[String]
media : String?
title : String?
title_star : ExtendedValue?
media_type : String?
extensions : Array[LinkParameter]
}

One parsed link. target is the URI-reference inside the angle brackets; relations the relation types of the rel parameter; anchor the optional overriding context; hreflang the repeatable language hint; media, title, title_star and media_type the serialisation-defined target attributes; extensions every other parameter in input order.

#
WebLink::anchor

fn WebLink::anchor(self : WebLink) -> String?

The optional overriding link context.

#
WebLink::extensions

fn WebLink::extensions(self : WebLink) -> Array[LinkParameter]

The extension parameters, in input order.

#
WebLink::has_relation

fn WebLink::has_relation(self : WebLink, name : String) -> Bool

Whether any relation of this link matches the given name or URI.

#
WebLink::hreflang

fn WebLink::hreflang(self : WebLink) -> Array[String]

The repeatable hreflang values.

#
WebLink::media

fn WebLink::media(self : WebLink) -> String?

The media target attribute, if present.

#
WebLink::media_type

fn WebLink::media_type(self : WebLink) -> String?

The type target attribute, if present.

#
WebLink::relation_names

fn WebLink::relation_names(self : WebLink) -> Array[String]

The relation names/URIs of this link as plain strings, in order.

#
WebLink::relations

fn WebLink::relations(self : WebLink) -> Array[RelationType]

The relation types of this link.

#
WebLink::target

fn WebLink::target(self : WebLink) -> String

The link target (URI-reference).

#
WebLink::title

fn WebLink::title(self : WebLink) -> String?

The title target attribute, if present.

#
WebLink::title_star

fn WebLink::title_star(self : WebLink) -> ExtendedValue?

The title* target attribute, if present.
fn audit_link_header(input : String) -> Result[AuditReport, LinkError]

Parses input as a Link header with the default limits and audits the result. Any parse error is returned unchanged.
fn audit_link_header_with_limits(input : String, limits : Limits) -> Result[AuditReport, LinkError]

Parses input as a Link header with the given limits and audits the result. Any parse error is returned unchanged.
fn audit_link_set(linkset : LinkSet) -> AuditReport

Audits a link set against the default resource limits.
fn audit_link_set_with_limits(linkset : LinkSet, limits : Limits) -> AuditReport

Audits a link set against the given resource limits. The parse-time max_links and max_input_bytes bounds do not apply here (the model is already in memory); the per-value bounds are still enforced.

#
can_be_token

fn can_be_token(value : String) -> Bool

Whether a string can be emitted as an unquoted token (all bytes are tchar and the string is non-empty). Used by the serializer to decide between the token and quoted forms.
fn canonical_link_header(input : String) -> Result[String, LinkError]

Canonicalizes a Link header field value via the model and re-serializes it. Equivalent to canonicalize_link_header; kept here so all conversions live in one module.
fn canonicalize_link_header(input : String) -> Result[String, LinkError]

Parses input with the default limits and returns its canonical Link header serialization.
fn canonicalize_link_header_with_limits(input : String, limits : Limits) -> Result[String, LinkError]

Parses input with the given limits and returns its canonical Link header serialization. Any parse error is returned unchanged.
fn canonicalize_link_value(input : String) -> Result[String, LinkError]

Canonicalizes a single link-value string.

#
filter_by_hreflang

fn filter_by_hreflang(links : Array[WebLink], lang : String) -> Array[WebLink]

All links whose hreflang values include lang (case-insensitive per RFC 5646 Section 2.1.1).

#
filter_by_relation

fn filter_by_relation(links : Array[WebLink], relation : String) -> Array[WebLink]

All links carrying the given relation, in order.

#
filter_by_type

fn filter_by_type(links : Array[WebLink], media_type : String) -> Array[WebLink]

All links whose type target attribute equals media_type (case-insensitive per RFC 6838 Section 4.2).

#
find_alternate

fn find_alternate(links : Array[WebLink]) -> Array[WebLink]

All links with relation alternate, in order.

#
find_by_relation

fn find_by_relation(links : Array[WebLink], relation : String) -> WebLink?

The first link carrying the given relation, or None.

#
find_canonical

fn find_canonical(links : Array[WebLink]) -> WebLink?

The first link with relation canonical, or None.

#
find_next

fn find_next(links : Array[WebLink]) -> WebLink?

The first link with relation next, or None.

#
find_prev

fn find_prev(links : Array[WebLink]) -> WebLink?

The first link with relation prev, or None.

#
is_alpha

fn is_alpha(b : Byte) -> Bool

ALPHA — ASCII letters.

#
is_bad_control

fn is_bad_control(b : Byte) -> Bool

Whether a byte is a vertical-tab or bare CR or LF (control characters that must never appear unquoted).

#
is_digit

fn is_digit(b : Byte) -> Bool

DIGIT — ASCII digits.

#
is_ext_value_name

fn is_ext_value_name(name : String) -> Bool

Whether the given parameter name is an RFC 8187 name* form.

#
is_hexdigit

fn is_hexdigit(b : Byte) -> Bool

HEXDIG — ASCII hex digits.

#
is_obs_text

fn is_obs_text(b : Byte) -> Bool

HTTP obs-text: bytes 0x80-0xFF. Allowed inside quoted-strings and quoted-pairs per RFC 7230, but rejected by the application/linkset text format (RFC 9264 Section 4.1).

#
is_registered_relation

fn is_registered_relation(name : String) -> Bool

Whether name is a registered IANA relation type. Matching is case-insensitive, per RFC 8288 Section 2.1.1.

#
library_version

fn library_version() -> String

The released library version. moon.mod mirrors this value; the CLI and any consumer that prints a version should use this function instead of hard-coding the string.
fn link_error(stage : LinkErrorStage, kind : LinkErrorKind, context : String) -> LinkError

Constructs a LinkError with the given stage, kind and context and a zero offset.
fn link_error_at(stage : LinkErrorStage, kind : LinkErrorKind, offset : Int, context : String) -> LinkError

Constructs a LinkError carrying an explicit UTF-8 byte offset.
fn link_header_to_linkset(input : String, limits : Limits) -> Result[LinkSet, LinkError]

Parses a Link header field value and wraps the result as a LinkSet.
fn link_header_to_linkset_json(input : String, limits : Limits) -> Result[String, LinkError]

Parses a Link header field value and returns it as an application/linkset+json document.
fn linkset_json_to_link_header(input : String, limits : Limits) -> Result[String, LinkError]

Parses an application/linkset+json document and returns its canonical Link header field value.

#
linkset_json_to_linkset_text

fn linkset_json_to_linkset_text(input : String, limits : Limits) -> Result[String, LinkError]

Parses an application/linkset+json document and returns it as an application/linkset document.
fn linkset_text_to_link_header(input : String, limits : Limits) -> Result[String, LinkError]

Parses an application/linkset document and returns its canonical Link header field value.

#
linkset_text_to_linkset_json

fn linkset_text_to_linkset_json(input : String, limits : Limits) -> Result[String, LinkError]

Parses an application/linkset document and returns it as an application/linkset+json document.
fn linkset_to_link_header(linkset : LinkSet) -> String

Serializes a LinkSet as a Link header field value (RFC 8288). This is the canonical, deterministic serialization shared by canonicalize_link_header and the CLI format subcommand.

#
linkset_to_linkset_text

fn linkset_to_linkset_text(linkset : LinkSet) -> String

Serializes a LinkSet as an application/linkset document.

#
max_context_bytes

fn max_context_bytes() -> Int

Maximum length of the context excerpt stored inside an error. Longer inputs are truncated so that errors never carry megabytes of input.

#
media_type_token_char

fn media_type_token_char(b : Byte) -> Bool

Whether a byte may appear in a media type token (RFC 6838 / RFC 7230 token). Same as token_char; kept as a named alias for readability.

#
parmchar

fn parmchar(b : Byte) -> Bool

RFC 8288 parmchar — allowed parameter-name characters.

#
parse_extended_value

fn parse_extended_value(cursor : Scanner, limits : Limits) -> ExtendedValue raise

Parses an extended value starting at the current scanner position. On success the cursor is positioned immediately after the last value-char. The value part must not contain whitespace, ;, or ,; those terminate the value.

Errors: ExtendedValue::UnexpectedCharacter (malformed charset or separator), ExtendedValue::InvalidPercentEncoding, ExtendedValue::InvalidUtf8 (percent-decoded bytes are not valid UTF-8), ExtendedValue::UnsupportedCharset, and ExtendedValue::InvalidLanguageTag.

#
parse_extended_value_string

fn parse_extended_value_string(input : String) -> Result[ExtendedValue, LinkError]

Parses a complete extended value from a string (the name* value, without the name or =).
fn parse_link_header(input : String, limits : Limits) -> Result[Array[WebLink], LinkError]

Parses a Link header field value into an array of links.

Errors: Input::EmptyInput (no link present), Input::LimitExceeded (input larger than max_input_bytes), Limit::LimitExceeded, plus the Target, Parameter, Relation, QuotedString, ExtendedValue, UriReference and Header errors raised while parsing a link.
fn parse_link_header_detailed(input : String, limits : Limits) -> Result[LinkHeaderParse, LinkError]

Parses a Link header field value, also returning the names of ignored duplicate parameters (see LinkHeaderParse).
fn parse_link_param(cursor : Scanner, limits : Limits) -> Result[ParsedParameter, LinkError]

Parses one link-parameter starting at the current scanner position. On success the cursor is positioned immediately after the value (or after the name, for flag parameters), before any following ; or OWS.

Errors: Parameter::MissingParameterName (no token where a name is required), Parameter::InvalidParameter (no value after =), Limit::LimitExceeded, plus the quoted-string and extended-value errors from parse_quoted_string and parse_extended_value.

#
parse_linkset_json

fn parse_linkset_json(input : String, limits : Limits) -> Result[LinkSet, LinkError]

Parses an application/linkset+json document into a LinkSet. Relation members are visited in sorted name order so the result is independent of JSON object key iteration order. Each (relation, target) pair becomes one link carrying a single relation type.

#
parse_linkset_text

fn parse_linkset_text(input : String, limits : Limits) -> Result[LinkSet, LinkError]

Parses an application/linkset document into a LinkSet. Newlines are accepted as separators; non-ASCII bytes are rejected (RFC 9264 Section 4.1).

#
parse_quoted_string

fn parse_quoted_string(cursor : Scanner, limits : Limits) -> String raise

Parses a quoted-string starting at the current scanner position (which must be the opening DQUOTE). On success the cursor is positioned after the closing DQUOTE. The returned string is the unquoted content with quoted-pairs resolved (\" becomes ", \\ becomes \).

Errors: UnterminatedQuotedString (no closing DQUOTE), InvalidQuotedPair (backslash not followed by a valid quoted-pair byte), UnexpectedCharacter (a control character that is not HTAB inside the string), LimitExceeded (the string is longer than max_quoted_string_bytes).

#
parse_relation_list

fn parse_relation_list(value : String, limits : Limits) -> Result[Array[RelationType], LinkError]

Parses a rel value (a space separated list of relation types) into an ordered array. Runs of SP are treated as a single separator, matching the 1*SP rule.

Errors: Relation::InvalidRelation and Limit::LimitExceeded.

#
parse_relation_type

fn parse_relation_type(token : String) -> Result[RelationType, LinkError]

Parses a single relation type from a non-empty token string.

If the string is a valid absolute URI it is classified as an extension relation type; otherwise it must be a valid token (RFC 7230 tchar) and is classified as a registered relation type.

Errors: Relation::InvalidRelation and Relation::InvalidExtensionRelation.

#
parse_uri_reference

fn parse_uri_reference(input : String) -> Result[UriReference, LinkError]

Parses and strictly validates a URI-reference.

Errors: UriReference::InvalidToken (character not allowed), and UriReference::InvalidPercentEncoding (a % not followed by two hex digits).

#
ptokenchar

fn ptokenchar(b : Byte) -> Bool

RFC 8288 ptokenchar — allowed unquoted parameter-value characters.

#
qdtext_char

fn qdtext_char(b : Byte) -> Bool

RFC 7230 qdtext: HTAB / SP / ! / #-[ / ]-~ / obs-text. Notably excludes " and \ and control characters.

#
quoted_pair_ok

fn quoted_pair_ok(b : Byte) -> Bool

A valid quoted-pair second byte: \ followed by HTAB / SP / VCHAR / obs-text (RFC 7230).

#
registered_relation_count

fn registered_relation_count() -> Int

The number of relation types in the offline snapshot.

#
registered_relations

fn registered_relations() -> Array[RelationInfo]

A fresh copy of every registry entry, sorted by relation type name (alphabetical, case-sensitive). The returned array is a copy: mutating it never affects later calls.

#
relation_info

fn relation_info(name : String) -> RelationInfo?

The registry entry for name, or None when no such relation type is registered. Matching is case-insensitive. The first (alphabetically earliest) match is returned, which is the canonical entry.

#
relation_type_is_extension

fn relation_type_is_extension(rt : RelationType) -> Bool

Whether a relation type is in the extension (absolute URI) form.

#
relation_type_is_registered

fn relation_type_is_registered(rt : RelationType) -> Bool

Whether a relation type is in the registered (token) form.

#
remove_dot_segments

fn remove_dot_segments(path : String) -> String

Implements remove_dot_segments (RFC 3986 Section 5.2.4) on a path.

The algorithm consumes the input path one rule at a time: leading ./ and ../ are dropped; /./ (or a trailing / .) collapses to /; /../ (or a trailing / ..) collapses to / and pops the last output segment; a path that is exactly . or .. is dropped; otherwise the first segment (including any leading /) is moved to the output. Every rule advances i, so the loop always terminates.

#
resolve_uri_reference

fn resolve_uri_reference(base : UriReference, reference : UriReference) -> UriReference

Resolves a reference against a base URI-reference, returning the resolved URI-reference model.

#
resolve_uri_reference_string

fn resolve_uri_reference_string(base : String, reference : String) -> Result[String, LinkError]

Resolves a reference against a base URI-reference, returning the resolved absolute URI-reference as a string. Implements RFC 3986 Section 5.2.2.

#
serialize_extended_value

fn serialize_extended_value(ev : ExtendedValue) -> String

Serialises an extended value deterministically. The charset is always emitted as UTF-8; the language is emitted when present; every value byte outside attr-char is percent-encoded with uppercase hex digits.
fn serialize_link(link : WebLink) -> String

Serializes one link as a single link-value.
fn serialize_link_header(links : Array[WebLink]) -> String

Serializes a list of links as a Link header field value (a comma-space separated list of link-values).
fn serialize_link_header_from_linkset(linkset : LinkSet) -> String

Serializes a LinkSet as a Link header field value.

#
serialize_linkset_json

fn serialize_linkset_json(linkset : LinkSet) -> String

Serializes a LinkSet as an application/linkset+json document. The output is deterministic (stable member order, UTF-8, escaped strings).

#
serialize_linkset_text

fn serialize_linkset_text(linkset : LinkSet) -> String

Serializes a LinkSet as an application/linkset document using the RFC 8288 field-value form (SP / HTAB separators, no newlines). The output is also a valid Link header field value.

#
serialize_linkset_text_multiline

fn serialize_linkset_text_multiline(linkset : LinkSet) -> String

Serializes a LinkSet as an application/linkset document with one link-value per line (newline separators, RFC 9264 Section 4.1). This is the readable form for a standalone document; when embedding the result in an HTTP header, replace the newlines with SP first.

#
serialize_quoted_string

fn serialize_quoted_string(value : String) -> String

Serialises a string as a quoted-string, deterministically. Only " and \ are escaped (with a backslash); every other byte is emitted as-is. The result always starts and ends with DQUOTE.

#
split_on_spaces

fn split_on_spaces(value : String) -> Array[String]

Splits a string on runs of SP (0x20). Consecutive SP bytes collapse into one separator; empty input yields an empty list.

#
targets_for_relation

fn targets_for_relation(links : Array[WebLink], relation : String) -> Array[String]

The targets of all links carrying the given relation, in order.

#
token_char

fn token_char(b : Byte) -> Bool

RFC 7230 tchar — allowed token characters.

#
uri_gen_delim

fn uri_gen_delim(b : Byte) -> Bool

RFC 3986 gen-delims: ":" / "/" / "?" / "#" / "[" / "]" / "@".

#
uri_pchar

fn uri_pchar(b : Byte) -> Bool

RFC 3986 pchar: unreserved / pct-encoded / sub-delims / ":" / "@". (Percent signs are handled separately by the URI parser.)

#
uri_sub_delim

fn uri_sub_delim(b : Byte) -> Bool

RFC 3986 sub-delims: "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=".

#
uri_unreserved

fn uri_unreserved(b : Byte) -> Bool

RFC 3986 unreserved: ALPHA / DIGIT / "-" / "." / "_" / "~".

#
valid_language_tag

fn valid_language_tag(tag : String) -> Bool

Validates an RFC 5646 Language-Tag. Implements the pragmatic subset: one or more subtags separated by -; the primary subtag is 2-8 letters (or x for private use); each extended subtag is 1-8 alphanumeric characters.
fn web_link(target : String) -> WebLink

Constructs a WebLink with the given target and no parameters.