@testitem "parser_functionality" tags = [:core] begin
    using CommonMark
    using Test
    # AST metadata via keywords.
    p = Parser()
    ast = p(""; empty = true)
    @test ast.meta["empty"] == true

    # Parsing file contents.
    readme = joinpath(@__DIR__, "../README.md")
    ast = open(p, readme)
    @test ast.meta["source"] == readme
    @test ast.first_child.t isa CommonMark.Heading

    # Parsing contents of a buffer.
    buffer = IOBuffer("# heading")
    ast = p(buffer)
    @test ast.first_child.t isa CommonMark.Heading
    @test markdown(ast) == "# heading\n"

    # Disabling parser rules.
    p = disable!(Parser(), CommonMark.AtxHeadingRule())
    ast = p("# *not a header*")
    @test ast.first_child.t isa CommonMark.Paragraph
    @test ast.first_child.first_child.nxt.t isa CommonMark.Emph
    @test markdown(ast) == "# *not a header*\n"

    # Make sure that enable! or disable! do not create duplicate rules
    # https://github.com/MichaelHatherly/CommonMark.jl/issues/45
    @test CommonMark.is_same_rule(LinkRule(), LinkRule())
    @test CommonMark.is_same_rule(FootnoteRule(), FootnoteRule())
    @test !CommonMark.is_same_rule(FootnoteRule(), LinkRule())
    let fn = CommonMark.is_same_rule(LinkRule())
        @test fn(LinkRule())
        @test !fn(FootnoteRule())
    end
    let fnrule1 = FootnoteRule(), fnrule2 = FootnoteRule()
        @test CommonMark.is_same_rule(fnrule1, fnrule2)
        fnrule1.cache["foo"] = CommonMark.Node()
        @test CommonMark.is_same_rule(fnrule1, fnrule2)
        fnrule2.cache["bar"] = CommonMark.Node()
        @test CommonMark.is_same_rule(fnrule1, fnrule2)
    end

    are_rules_unique(p::Parser) = p.rules == unique(p.rules)
    let p = Parser()
        @test CommonMark.ruleoccursin(LinkRule(), p.rules)
        @test CommonMark.ruleoccursin(ImageRule(), p.rules)
        @test !CommonMark.ruleoccursin(TableRule(), p.rules)
        @test !CommonMark.ruleoccursin(FootnoteRule(), p.rules)
        @test are_rules_unique(p)
    end
    let p = enable!(Parser(), TableRule())
        @test CommonMark.ruleoccursin(LinkRule(), p.rules)
        @test CommonMark.ruleoccursin(ImageRule(), p.rules)
        @test CommonMark.ruleoccursin(TableRule(), p.rules)
        @test !CommonMark.ruleoccursin(FootnoteRule(), p.rules)
        @test are_rules_unique(p)
    end
    let p = enable!(Parser(), [TableRule(), FootnoteRule()])
        @test CommonMark.ruleoccursin(LinkRule(), p.rules)
        @test CommonMark.ruleoccursin(ImageRule(), p.rules)
        @test CommonMark.ruleoccursin(TableRule(), p.rules)
        @test CommonMark.ruleoccursin(FootnoteRule(), p.rules)
        @test are_rules_unique(p)
    end
    @test_throws ErrorException enable!(Parser(), LinkRule())
    @test_throws ErrorException enable!(Parser(), [LinkRule(), ImageRule()])
    @test_throws ErrorException enable!(Parser(), [LinkRule(), FootnoteRule()])
    let p = disable!(Parser(), LinkRule())
        @test !CommonMark.ruleoccursin(LinkRule(), p.rules)
        @test CommonMark.ruleoccursin(ImageRule(), p.rules)
        @test !CommonMark.ruleoccursin(TableRule(), p.rules)
        @test !CommonMark.ruleoccursin(FootnoteRule(), p.rules)
        @test are_rules_unique(p)
    end
    let p = disable!(Parser(), [LinkRule(), ImageRule()])
        @test !CommonMark.ruleoccursin(LinkRule(), p.rules)
        @test !CommonMark.ruleoccursin(ImageRule(), p.rules)
        @test !CommonMark.ruleoccursin(TableRule(), p.rules)
        @test !CommonMark.ruleoccursin(FootnoteRule(), p.rules)
        @test are_rules_unique(p)
    end
    let p = disable!(Parser(), TableRule())
        @test CommonMark.ruleoccursin(LinkRule(), p.rules)
        @test CommonMark.ruleoccursin(ImageRule(), p.rules)
        @test !CommonMark.ruleoccursin(TableRule(), p.rules)
        @test !CommonMark.ruleoccursin(FootnoteRule(), p.rules)
        @test are_rules_unique(p)
    end
    let p = disable!(Parser(), [TableRule(), FootnoteRule()])
        @test CommonMark.ruleoccursin(LinkRule(), p.rules)
        @test CommonMark.ruleoccursin(ImageRule(), p.rules)
        @test !CommonMark.ruleoccursin(TableRule(), p.rules)
        @test !CommonMark.ruleoccursin(FootnoteRule(), p.rules)
        @test are_rules_unique(p)
    end
    let p = disable!(Parser(), [LinkRule(), FootnoteRule()])
        @test !CommonMark.ruleoccursin(LinkRule(), p.rules)
        @test CommonMark.ruleoccursin(ImageRule(), p.rules)
        @test !CommonMark.ruleoccursin(TableRule(), p.rules)
        @test !CommonMark.ruleoccursin(FootnoteRule(), p.rules)
        @test are_rules_unique(p)
    end
    # Parser constructor with enable/disable keywords
    let p = Parser(enable = [TableRule()])
        @test CommonMark.ruleoccursin(TableRule(), p.rules)
        @test CommonMark.ruleoccursin(LinkRule(), p.rules)
        @test are_rules_unique(p)
    end
    let p = Parser(enable = [TableRule(), FootnoteRule()])
        @test CommonMark.ruleoccursin(TableRule(), p.rules)
        @test CommonMark.ruleoccursin(FootnoteRule(), p.rules)
        @test are_rules_unique(p)
    end
    let p = Parser(disable = [LinkRule()])
        @test !CommonMark.ruleoccursin(LinkRule(), p.rules)
        @test CommonMark.ruleoccursin(ImageRule(), p.rules)
        @test are_rules_unique(p)
    end
    let p = Parser(disable = [LinkRule(), ImageRule()])
        @test !CommonMark.ruleoccursin(LinkRule(), p.rules)
        @test !CommonMark.ruleoccursin(ImageRule(), p.rules)
        @test are_rules_unique(p)
    end
    let p = Parser(enable = [TableRule()], disable = [LinkRule()])
        @test CommonMark.ruleoccursin(TableRule(), p.rules)
        @test !CommonMark.ruleoccursin(LinkRule(), p.rules)
        @test are_rules_unique(p)
    end
    # Functional test: disabled rule doesn't parse
    let p = Parser(disable = [AtxHeadingRule()])
        ast = p("# not a header")
        @test ast.first_child.t isa CommonMark.Paragraph
    end
end

@testitem "reference definition discards invalid title" tags = [:core] begin
    using CommonMark
    using Test
    p = Parser()

    # A title followed by other text on the line is invalid; the definition is
    # still valid using the URL alone, and the title must be discarded.
    ast = p("[foo]: /url\n\"title\" extra\n\n[foo]\n")
    @test occursin("<a href=\"/url\">foo</a>", html(ast))
    @test !occursin("title=", html(ast))

    # Control: a clean title on its own line is kept.
    ast = p("[foo]: /url\n\"title\"\n\n[foo]\n")
    @test occursin("<a href=\"/url\" title=\"title\">foo</a>", html(ast))
end

@testitem "link destination rejects unbalanced parens" tags = [:core] begin
    using CommonMark
    using Test
    p = Parser()

    # An unparenthesised destination with an unbalanced '(' is not a valid link.
    ast = p("[a](b(c )\n")
    @test !occursin("<a href", html(ast))
    @test occursin("[a](b(c )", html(ast))

    # Control: balanced parens still form a link.
    ast = p("[a](b(c) )\n")
    @test occursin("<a href=\"b(c)\">a</a>", html(ast))
end

@testitem "link label length boundary" tags = [:core] begin
    using CommonMark
    using Test
    p = Parser()

    # The spec allows up to 999 characters inside the brackets.
    label999 = repeat("a", 999)
    ast = p("[$label999]\n\n[$label999]: /url\n")
    @test occursin("<a href=\"/url\">", html(ast))

    # 1000 characters exceeds the limit and must not resolve.
    label1000 = repeat("a", 1000)
    ast = p("[$label1000]\n\n[$label1000]: /url\n")
    @test !occursin("<a href", html(ast))
end

@testitem "numeric entity validity" tags = [:core] begin
    using CommonMark
    using Test
    p = Parser()
    decode(s) = p(s).first_child.first_child.literal

    # Surrogate and out-of-range codepoints must decode to U+FFFD.
    @test decode("&#xD800;") == "�"
    @test decode("&#1114112;") == "�"

    # Control: valid codepoints decode normally.
    @test decode("&#65;") == "A"
    @test decode("&#x10FFFF;") == "\U10FFFF"
end

@testitem "inline_handler_preconditions" tags = [:core] begin
    using CommonMark
    using Test

    # Dispatch-entry handlers rely on a non-local guarantee: the inline parser
    # peeks the trigger char before calling the handler. The handler must fail
    # fast if that precondition is violated, not silently consume the wrong char.
    inline_at = function (s)
        p = CommonMark.InlineParser()
        p.buf = s
        p.pos = 1
        p.len = ncodeunits(s)
        return p
    end
    block = CommonMark.Node(CommonMark.Document())

    @test_throws ErrorException CommonMark.parse_backslash(inline_at("x"), block)
    @test_throws ErrorException CommonMark.parse_newline(inline_at("x"), block)
    @test_throws ErrorException CommonMark.parse_open_bracket(inline_at("x"), block)
end
