001package org.jsoup.nodes;
002
003import org.jsoup.helper.Validate;
004import org.jsoup.internal.QuietAppendable;
005import org.jsoup.internal.StringUtil;
006import org.jsoup.parser.Parser;
007
008/**
009 A processing instruction with a target and data, such as {@code <?marker name="country-options"?>}.
010
011 <p>The XML declaration {@code <?xml ...?>} is represented by {@link XmlDeclaration}.</p>
012 <p>If you access this node's attributes, directly or through an attribute selector, the instruction data is parsed into an {@link Attributes} object using the owning document's parser settings, or HTML settings if the node is detached.
013 The attributes then determine the instruction data.</p>
014
015 @since 1.24.1 */
016public class ProcessingInstruction extends LeafNode {
017    private final String target;
018    private int dataStartPos = -1; // source offset used to rebase parsed attribute ranges; -1 if untracked
019
020    /**
021     Creates a processing instruction with the supplied target and data.
022     @param target the target following {@code <?}
023     @param data the data following the target, without the separating whitespace or closing {@code ?>}
024     */
025    public ProcessingInstruction(String target, String data) {
026        super(data);
027        Validate.notNull(target);
028        this.target = target;
029    }
030
031    /** Returns the node name {@code #processing-instruction}. */
032    @Override public String nodeName() {
033        return "#processing-instruction";
034    }
035
036    /**
037     Returns the instruction target.
038     @return the target following {@code <?}
039     */
040    public String target() {
041        return target;
042    }
043
044    /**
045     Returns the data following the target.
046     If the attributes have been accessed, returns their serialized form.
047     @return the instruction data
048     */
049    public String data() {
050        if (!hasAttributes()) return coreValue();
051
052        Document owner = ownerDocument();
053        Document.OutputSettings out = owner != null ? owner.outputSettings() : new Document.OutputSettings();
054        return data(out);
055    }
056
057    /** Serializes the parsed instruction attributes. */
058    private String data(Document.OutputSettings out) {
059        StringBuilder data = StringUtil.borrowBuilder();
060        attributes().html(QuietAppendable.wrap(data), out);
061        if (data.length() > 0)
062            data.deleteCharAt(0); // drop the separator Attributes writes before each attribute
063        return StringUtil.releaseBuilder(data);
064    }
065
066    /**
067     Replaces the instruction data.
068     If the attributes have been accessed, parses the new data and updates the existing {@link Attributes} object.
069     @param data the new data, without the separating whitespace or closing {@code ?>}
070     @return this node, for chaining
071     */
072    public ProcessingInstruction data(String data) {
073        Validate.notNull(data);
074        dataStartPos = -1;
075        if (hasAttributes()) {
076            Attributes parsed = parseAttributes(data);
077            super.clearAttributes();
078            attributes().addAll(parsed);
079        } else {
080            coreValue(data);
081        }
082        return this;
083    }
084
085    /** Returns the instruction data as this node's value. */
086    @Override public String nodeValue() {
087        return data();
088    }
089
090    /** Parses the instruction data into attribute storage when requested. */
091    @Override Attributes newAttributes(String data) {
092        return parseAttributes(data);
093    }
094
095    /** Parses the instruction data without changing the owning document's parser state. */
096    private Attributes parseAttributes(String data) {
097        Document owner = ownerDocument();
098        Parser parser = owner != null ? owner.parser().newInstance() : Parser.htmlParser();
099        Range sourceRange = sourceRange();
100        boolean trackPosition = dataStartPos >= 0 && sourceRange.isTracked();
101        parser.setTrackPosition(trackPosition);
102        Attributes attributes = parser.parseAttributes(data);
103        if (trackPosition) {
104            Range.Spans spans = attributes.spans();
105            if (spans != null)
106                spans.rebaseAttributeRanges(sourceRange, dataStartPos);
107        }
108        return attributes;
109    }
110
111    void sourceDataStart(int pos) {
112        dataStartPos = pos;
113    }
114
115    @Override public String attr(String key) {
116        ensureAttributes();
117        return super.attr(key);
118    }
119
120    @Override public ProcessingInstruction attr(String key, String value) {
121        ensureAttributes();
122        super.attr(key, value);
123        return this;
124    }
125
126    @Override public int attributesSize() {
127        return attributes().size();
128    }
129
130    @Override public ProcessingInstruction clearAttributes() {
131        ensureAttributes();
132        super.clearAttributes();
133        return this;
134    }
135
136    @Override void outerHtmlHead(QuietAppendable accum, Document.OutputSettings out) {
137        String data = hasAttributes() ? data(out) : coreValue();
138        accum.append("<?").append(target);
139        if (out.syntax() == Document.OutputSettings.Syntax.html || !data.isEmpty())
140            accum.append(' ');
141        accum.append(data).append("?>");
142    }
143
144    @Override public ProcessingInstruction clone() {
145        return (ProcessingInstruction) super.clone();
146    }
147}