#@linkify

    Detect URLs and email addresses inside plain text — and optionally wrap them in <a> elements inside an existing DOM tree.

    The examples below are mbt check blocks and run as part of moon test linkify.

    find_links returns an array of LinkMatch records. start and end are UTF-16 code-unit offsets into the input; text is the matched substring with trailing punctuation trimmed; href is the URL the linker would emit (with an http:// or mailto: scheme synthesized when needed).

    ///|
    test "readme find_links basic" {
    debug_inspect(
    @linkify.find_links("Visit https://example.com today."),
    content=(
    #|[
    #| {
    #| start: 6,
    #| end: 25,
    #| text: "https://example.com",
    #| href: "https://example.com",
    #| kind: "url",
    #| },
    #|]
    ),
    )
    }

    Bare domains, emails, and protocol-relative URLs all work:

    ///|
    test "readme find_links variants" {
    debug_inspect(
    @linkify.find_links(
    "see example.com or mail me@example.org or //cdn.example/x",
    ),
    content=(
    #|[
    #| {
    #| start: 4,
    #| end: 15,
    #| text: "example.com",
    #| href: "http://example.com",
    #| kind: "url",
    #| },
    #| {
    #| start: 24,
    #| end: 38,
    #| text: "me@example.org",
    #| href: "mailto:me@example.org",
    #| kind: "email",
    #| },
    #| {
    #| start: 42,
    #| end: 57,
    #| text: "//cdn.example/x",
    #| href: "//cdn.example/x",
    #| kind: "url",
    #| },
    #|]
    ),
    )
    }

    #Customizing the matcher

    LinkifyConfig gates two opt-ins: fuzzy_ip recognizes bare IPv4 addresses as URLs, and extra_tlds extends the list of recognized top-level domains for fuzzy (no-scheme) matches.

    ///|
    test "readme find_links_with_config extra_tlds" {
    // `internal` isn't a real TLD; default matcher ignores it.
    let plain = @linkify.find_links("site.internal")
    let custom = @linkify.find_links_with_config(
    "site.internal",
    @linkify.LinkifyConfig::with_extra_tlds(["internal"]),
    )
    debug_inspect(
    (plain.length(), custom.length()),
    content=(
    #|(0, 1)
    ),
    )
    }

    #Linkifying a DOM tree

    linkify_dom walks a DOM node and wraps every link-like substring in its text descendants with an <a> element. Text inside an existing anchor, <code>, <pre>, <script>, <style>, or <textarea> is skipped by default.

    ///|
    test "readme linkify_dom basic" {
    let fragment = @dom.fragment(children=[
    @dom.text("Email me@example.com or visit example.com"),
    ])
    let linked = @linkify.linkify_dom(fragment)
    inspect(
    @ser.to_html(linked, pretty=false),
    content=(
    #|Email <a href="mailto:me@example.com">me@example.com</a> or visit <a href="http://example.com">example.com</a>
    ),
    )
    }

    The default skip list:

    ///|
    test "readme linkify_default_dom_skip_tags" {
    debug_inspect(
    @linkify.linkify_default_dom_skip_tags(),
    content=(
    #|["a", "code", "pre", "script", "style", "textarea"]
    ),
    )
    }

    Override it by passing skip_tags:

    ///|
    test "readme linkify_dom custom skip_tags" {
    let kbd = @dom.element("kbd", children=[@dom.text("press example.com")])
    let linked = @linkify.linkify_dom(@dom.fragment(children=[kbd]), skip_tags=[
    "kbd",
    ])
    inspect(
    @ser.to_html(linked, pretty=false),
    content=(
    #|<kbd>press example.com</kbd>
    ),
    )
    }

    LinkMatch

    pub(all) struct LinkMatch {
    start : Int
    end : Int
    text : String
    href : String
    kind : String
    } derive(Eq,
    Debug
    )

    A URL or email-like span found in plain text.

    start and end are UTF-16 offsets into the input StringView. kind is "url" or "email".

    LinkMatch::equal

    #deprecated("implicit derived-impl promotion; call the trait method directly")
    fn LinkMatch::equal(LinkMatch, LinkMatch) -> Bool

    LinkMatch::not_equal

    #deprecated("implicit derived-impl promotion; call the trait method directly")
    fn LinkMatch::not_equal(x : LinkMatch, y : LinkMatch) -> Bool

    LinkMatch::to_repr

    #deprecated("implicit derived-impl promotion; call the trait method directly")
    fn LinkMatch::to_repr(LinkMatch) ->
    Repr

    LinkifyConfig

    pub struct LinkifyConfig {
    // private fields
    } derive(
    Debug
    )

    Options for the plain-text link scanner.

    LinkifyConfig::LinkifyConfig

    fn LinkifyConfig::LinkifyConfig(fuzzy_ip? : Bool, extra_tlds? : Array[String]) -> LinkifyConfig

    Create a linkify scanner configuration.

    fuzzy_ip enables bare IPv4 address matches such as 192.168.0.1. extra_tlds extends the default fuzzy domain and email TLD allowlist.

    LinkifyConfig::to_repr

    #deprecated("implicit derived-impl promotion; call the trait method directly")
    fn LinkifyConfig::to_repr(LinkifyConfig) ->
    Repr

    LinkifyConfig::with_extra_tlds

    fn LinkifyConfig::with_extra_tlds(extra_tlds : Array[String]) -> LinkifyConfig

    Create a linkify scanner configuration with additional fuzzy-link TLDs.
    fn find_links(text : StringView) -> Array[LinkMatch]

    Find URL and email-like spans in plain text using the default configuration.
    fn find_links_with_config(text : StringView, config : LinkifyConfig) -> Array[LinkMatch]

    Find URL and email-like spans in plain text using an explicit configuration.

    linkify_default_dom_skip_tags

    fn linkify_default_dom_skip_tags() -> Array[String]

    Return the default element names whose descendants are not linkified.

    The list includes existing anchors and text contexts where automatic link insertion would change code, raw text, or whitespace-preserving content.

    linkify_dom

    fn linkify_dom(node :
    Node
    , config? : LinkifyConfig, skip_tags? : Array[String]) ->
    Node

    Linkify URL and email text inside a DOM subtree.

    The transform mutates and returns node. By default it skips existing anchors and whitespace-preserving tags (code, pre, script, style, and textarea), while still processing normal children such as template contents.