"""
    AttributeRule()

Parse attribute syntax to attach metadata to elements.

Block attributes go above the target element. Inline attributes go after.
Uses `{#id .class key=value}` syntax.

```markdown
{#custom-id .highlight}
# Heading

{.note}
Paragraph with attributes.

[Link](url){target=_blank}
```
"""
struct AttributeRule
    nodes::Vector{Node}
    AttributeRule() = new([])
end

struct Attributes <: AbstractBlock
    dict::Dict{String, Any}
    block::Bool
end

is_container(::Attributes) = false
accepts_lines(::Attributes) = false
continue_(::Attributes, ::Parser, ::Node) = 1
finalize(::Attributes, ::Parser, ::Node) = nothing
can_contain(::Attributes, t) = false

function parse_block_attributes(parser::Parser, container::Node)
    # Block attributes mustn't appear directly after another attribute block.
    if !parser.indented &&
            (isnull(container.last_child) || !(container.last_child.t isa Attributes))
        dict, literal = try_parse_attributes(parser)
        if dict !== nothing
            advance_next_nonspace(parser)
            advance_offset(parser, length(literal), false)
            close_unmatched_blocks(parser)
            child = add_child(parser, Attributes(dict, true), parser.next_nonspace)
            child.literal = literal
            advance_to_end(parser)
            return 2
        end
    end
    return 0
end

block_rule(::AttributeRule) = Rule(parse_block_attributes, 1, "{")

inline_rule(rule::AttributeRule) = Rule(1, "{") do parser, block
    isnull(block.first_child) && return false # Can't have inline attribute as first in block.
    dict, literal = try_parse_attributes(parser)
    dict === nothing && return false
    node = Node(Attributes(dict, false))
    node.literal = literal
    push!(rule.nodes, node)
    append_child(block, node)
    return true
end

function try_parse_attributes(parser::AbstractParser)
    start_mark = pos = position(parser)
    while peek(parser, Char) === ' '
        # Consume leading spaces.
        read(parser, Char)
    end
    c = read(parser, Char)
    c === '{' || error("expected '{' opening attributes, got $(repr(c))")
    valid = false
    dict = Dict{String, Any}()
    key = ""
    while !eof(parser)
        pos = position(parser)
        char = read(parser, Char)
        if char === '}'
            valid = key == ""
            break
        elseif isspace(char)
            continue
        elseif char === '='
            isempty(key) && break
        elseif char === ':'
            key = "element"
        elseif char === '#'
            key = "id"
        elseif char === '.'
            key = "class"
        elseif isletter(char) || isnumeric(char) || char in "-_"
            mark = pos
            while !eof(parser)
                pos = position(parser)
                char = peek(parser, Char)
                if isletter(char) || isnumeric(char) || char in "-_"
                    read(parser, Char)
                else
                    break
                end
            end
            word = chop(String(bytes(parser, mark, pos)); tail = 1)
            if isempty(key)
                # Keys can't start with a number.
                startswith(word, r"[0-9]") && break
                key = word
                # Check for empty attribute syntax.
                if !startswith(parser, r"\s*=")
                    dict[key] = ""
                    key = ""
                end
            else
                if key == "class"
                    push!(get!(() -> String[], dict, key), word)
                else
                    dict[key] = word
                end
                key = ""
            end
        elseif char in "'\""
            # Simple strings, no escaping done.
            delimiter = char
            mark = pos
            while !eof(parser)
                pos = position(parser)
                char = read(parser, Char)
                if char === delimiter
                    break
                end
            end
            if isempty(key)
                # Strings can't be keys, so fail to parse.
                break
            else
                str = chop(String(bytes(parser, mark, pos)); head = 1, tail = 1)
                if key == "class"
                    push!(get!(() -> String[], dict, key), str)
                else
                    dict[key] = str
                end
                key = ""
            end
        else
            break
        end
    end
    if valid
        literal = String(bytes(parser, start_mark, pos))
        return dict, literal
    else
        seek(parser, start_mark)
        return nothing, ""
    end
end

block_modifier(::AttributeRule) = Rule(1) do parser, node
    if node.t isa Attributes && !isnull(node.nxt)
        node.nxt.t isa Attributes || (node.nxt.meta = node.t.dict)
    end
    return nothing
end

inline_modifier(rule::AttributeRule) = Rule(1) do parser, block
    while !isempty(rule.nodes)
        node = pop!(rule.nodes)
        if !isnull(node.prv) && !(node.prv.t isa Attributes)
            node.prv.meta = node.t.dict
        end
    end
end

# Writers.

write_html(::Attributes, w, n, ent) = nothing
write_latex(::Attributes, w, n, ent) = nothing
write_typst(::Attributes, w, n, ent) = nothing
write_term(::Attributes, w, n, ent) = nothing

function write_markdown(at::Attributes, w, n, ent)
    return if ent
        at.block && print_margin(w)
        literal(w, n.literal, at.block ? "\n" : "")
    end
end

write_json(::Attributes, ctx, node, enter) = nothing
