1 | // Copyright (C) 2007 Google Inc. |
2 | // |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | // you may not use this file except in compliance with the License. |
5 | // You may obtain a copy of the License at |
6 | // |
7 | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | // |
9 | // Unless required by applicable law or agreed to in writing, software |
10 | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | // See the License for the specific language governing permissions and |
13 | // limitations under the License. |
14 | |
15 | package com.google.caja.parser.html; |
16 | |
17 | import com.google.caja.SomethingWidgyHappenedError; |
18 | import com.google.caja.lexer.FilePosition; |
19 | import com.google.caja.lexer.HtmlEntities; |
20 | import com.google.caja.lexer.HtmlTokenType; |
21 | import com.google.caja.lexer.Token; |
22 | import com.google.caja.reporting.Message; |
23 | import com.google.caja.reporting.MessageLevel; |
24 | import com.google.caja.reporting.MessagePart; |
25 | import com.google.caja.reporting.MessageQueue; |
26 | import com.google.caja.reporting.MessageType; |
27 | import com.google.caja.util.Lists; |
28 | import com.google.caja.util.Maps; |
29 | import com.google.caja.util.Strings; |
30 | |
31 | import java.util.Collections; |
32 | import java.util.List; |
33 | import java.util.Map; |
34 | import java.util.WeakHashMap; |
35 | import java.util.logging.Level; |
36 | import java.util.logging.Logger; |
37 | import java.util.regex.Matcher; |
38 | import java.util.regex.Pattern; |
39 | |
40 | import org.w3c.dom.Attr; |
41 | import org.w3c.dom.DOMException; |
42 | import org.w3c.dom.Document; |
43 | import org.w3c.dom.DocumentFragment; |
44 | import org.w3c.dom.Element; |
45 | import org.w3c.dom.NamedNodeMap; |
46 | import org.w3c.dom.Node; |
47 | import org.w3c.dom.Text; |
48 | import org.xml.sax.ErrorHandler; |
49 | import org.xml.sax.SAXException; |
50 | import org.xml.sax.SAXParseException; |
51 | |
52 | import nu.validator.htmlparser.common.DoctypeExpectation; |
53 | import nu.validator.htmlparser.common.XmlViolationPolicy; |
54 | import nu.validator.htmlparser.impl.AttributeName; |
55 | import nu.validator.htmlparser.impl.ElementName; |
56 | import nu.validator.htmlparser.impl.HtmlAttributes; |
57 | import nu.validator.htmlparser.impl.Tokenizer; |
58 | |
59 | /** |
60 | * A bridge between DomParser and html5lib which translates |
61 | * {@code Token<HtmlTokenType>}s into SAX style events which are fed to the |
62 | * TreeBuilder. The TreeBuilder responds by issuing {@code createElementNS} |
63 | * commands which are used to build a {@link DocumentFragment}. |
64 | * |
65 | * @author mikesamuel@gmail.com |
66 | */ |
67 | public class Html5ElementStack implements OpenElementStack { |
68 | public static final Logger logger = Logger.getLogger( |
69 | Html5ElementStack.class.getName()); |
70 | private final CajaTreeBuilder builder; |
71 | private final char[] charBuf = new char[1024]; |
72 | private final MessageQueue mq; |
73 | private final Document doc; |
74 | private final boolean needsDebugData; |
75 | private final Map<String, ElementName> elNames = Maps.newHashMap(); |
76 | private boolean isFragment; |
77 | private boolean needsNamespaceFixup; |
78 | private boolean topLevelHtmlFromInput = false; |
79 | private boolean processingFirstTag = true; |
80 | |
81 | /** |
82 | * @param doc The document being processed. |
83 | * @param needsDebugData see {@link DomParser#setNeedsDebugData(boolean)} |
84 | * @param queue will receive error messages from html5lib. |
85 | */ |
86 | Html5ElementStack(Document doc, boolean needsDebugData, MessageQueue queue) { |
87 | this.doc = doc; |
88 | this.needsDebugData = needsDebugData; |
89 | this.mq = queue; |
90 | builder = new CajaTreeBuilder(doc, needsDebugData, mq); |
91 | } |
92 | |
93 | public final Document getDocument() { return doc; } |
94 | |
95 | public boolean needsNamespaceFixup() { return needsNamespaceFixup; } |
96 | |
97 | /** {@inheritDoc} */ |
98 | public void open(boolean isFragment) { |
99 | this.isFragment = isFragment; |
100 | if (isFragment) { |
101 | builder.setFragmentContext(null); |
102 | } |
103 | builder.setDoctypeExpectation(DoctypeExpectation.NO_DOCTYPE_ERRORS); |
104 | try { |
105 | builder.startTokenization(new Tokenizer(builder)); |
106 | } catch (SAXException ex) { |
107 | throw new SomethingWidgyHappenedError(ex); |
108 | } |
109 | builder.setErrorHandler( |
110 | new ErrorHandler() { |
111 | private FilePosition lastPos; |
112 | private String lastMessage; |
113 | |
114 | public void error(SAXParseException ex) { |
115 | // htmlparser is a bit strident, so we lower it's warnings to |
116 | // MessageLevel.LINT. |
117 | report(MessageLevel.LINT, ex); |
118 | } |
119 | public void fatalError(SAXParseException ex) { |
120 | report(MessageLevel.FATAL_ERROR, ex); |
121 | } |
122 | public void warning(SAXParseException ex) { |
123 | report(MessageLevel.LINT, ex); |
124 | } |
125 | |
126 | private void report(MessageLevel level, SAXParseException ex) { |
127 | String message = errorMessage(ex); |
128 | FilePosition pos = builder.getErrorLocation(); |
129 | if (message.equals(lastMessage) && pos.equals(lastPos)) { return; } |
130 | lastMessage = message; |
131 | lastPos = pos; |
132 | mq.getMessages().add(new Message( |
133 | DomParserMessageType.GENERIC_SAX_ERROR, level, pos, |
134 | MessagePart.Factory.valueOf(message))); |
135 | } |
136 | |
137 | private String errorMessage(SAXParseException ex) { |
138 | // Don't ask. |
139 | return ex.getMessage() |
140 | .replace('\u201c', '\'').replace('\u201d', '\''); |
141 | } |
142 | }); |
143 | } |
144 | |
145 | /** {@inheritDoc} */ |
146 | public void finish(FilePosition endOfFile) { |
147 | if (CajaTreeBuilder.DEBUG) { |
148 | System.err.println("finish(" + endOfFile + ")"); |
149 | } |
150 | builder.finish(endOfFile); |
151 | if (CajaTreeBuilder.DEBUG) { |
152 | System.err.println("closeUnclosedNodes"); |
153 | } |
154 | builder.closeUnclosedNodes(); |
155 | } |
156 | |
157 | public static String canonicalizeName(String name) { |
158 | if (name.indexOf(':') >= 0) { // Do not case-normalize embedded XML. |
159 | return name; |
160 | } else { |
161 | // Forces LANG=C like behavior. |
162 | return Strings.toLowerCase(name); |
163 | } |
164 | } |
165 | |
166 | static String canonicalElementName(String elementName) { |
167 | return canonicalizeName(elementName); |
168 | } |
169 | |
170 | static String canonicalAttributeName(String attributeName) { |
171 | return canonicalizeName(attributeName); |
172 | } |
173 | |
174 | /** {@inheritDoc} */ |
175 | public DocumentFragment getRootElement() { |
176 | // libHtmlParser always produces a document with html, head, and body tags |
177 | // which we usually don't want, so unroll it. |
178 | |
179 | // If we can't throw away the head element, and the body header, then we |
180 | // return the entire document. Otherwise, we return a document fragment |
181 | // consisting of the contents of the body. |
182 | |
183 | Element root = builder.getRootElement(); |
184 | DocumentFragment result = doc.createDocumentFragment(); |
185 | if (needsDebugData) { |
186 | Nodes.setFilePositionFor(result, builder.getFragmentBounds()); |
187 | } |
188 | |
189 | final Node first = root.getFirstChild(); |
190 | |
191 | if (!isFragment || topLevelHtmlFromInput) { |
192 | result.appendChild(root); |
193 | return result; |
194 | } |
195 | |
196 | // If disposing of the html, body, or head elements would lose info don't |
197 | // do it, so look for attributes. |
198 | boolean tagsBesidesHeadBodyFrameset = false; |
199 | boolean topLevelTagsWithAttributes = hasSpecifiedAttributes(root); |
200 | |
201 | for (Node child = first; child != null; child = child.getNextSibling()) { |
202 | if (child instanceof Element) { |
203 | Element el = (Element) child; |
204 | String tagName = el.getTagName(); |
205 | if (!("head".equals(tagName) || "body".equals(tagName) |
206 | || "frameset".equals(tagName))) { |
207 | tagsBesidesHeadBodyFrameset = true; |
208 | break; |
209 | } |
210 | if (!topLevelTagsWithAttributes |
211 | && hasSpecifiedAttributes(el) |
212 | // framesets, unlike body elements, are never created out of whole |
213 | // cloth, so we do not behave differently when there is a frameset |
214 | // with attributes. |
215 | && !"frameset".equals(tagName)) { |
216 | topLevelTagsWithAttributes = true; |
217 | } |
218 | } |
219 | } |
220 | |
221 | // topLevelTagsWithAttributes is true in the following cases |
222 | // <html xml:lang="en">...</html> |
223 | // <html><body bgcolor=white>...</body></html> |
224 | // tagsBesidesHeadAndBody is true for |
225 | // <html><frameset>...</frameset></html> |
226 | if (tagsBesidesHeadBodyFrameset || topLevelTagsWithAttributes) { |
227 | // Merging the body and head would lose info. |
228 | result.appendChild(root); |
229 | return result; |
230 | } |
231 | |
232 | // Merge the body and head into a fragment. |
233 | // Convert |
234 | // <html> |
235 | // <head> |
236 | // <link rel=stylesheet ...> |
237 | // </head> |
238 | // <body> |
239 | // <p>Hello World</p. |
240 | // </body> |
241 | // </html> |
242 | // to |
243 | // #fragment |
244 | // <link rel=stylesheet ...> |
245 | // <p>Hello World</p. |
246 | |
247 | Node pending = null; |
248 | for (Node child = first; child != null; child = child.getNextSibling()) { |
249 | if (child instanceof Element) { |
250 | String tagName = ((Element) child).getTagName(); |
251 | if ("head".equals(tagName) || "body".equals(tagName)) { |
252 | // Shallow descent |
253 | for (Node grandchild = child.getFirstChild(); grandchild != null; |
254 | grandchild = grandchild.getNextSibling()) { |
255 | pending = appendNormalized(pending, grandchild, result); |
256 | } |
257 | } else { // reached for framesets |
258 | pending = child; |
259 | } |
260 | } else { |
261 | pending = appendNormalized(pending, child, result); |
262 | } |
263 | } |
264 | if (pending != null) { result.appendChild(pending); } |
265 | |
266 | return result; |
267 | } |
268 | |
269 | private static boolean hasSpecifiedAttributes(Element el) { |
270 | NamedNodeMap attrs = el.getAttributes(); |
271 | for (int i = 0, n = attrs.getLength(); i < n; ++i) { |
272 | Attr a = (Attr) attrs.item(i); |
273 | if (el.hasAttributeNS(a.getNamespaceURI(), a.getLocalName())) { |
274 | return true; |
275 | } |
276 | } |
277 | return false; |
278 | } |
279 | |
280 | /** |
281 | * Given one or two nodes, see if the two can be combined. |
282 | * If two are passed in, they might be combined into one and returned, or |
283 | * the first will be appended to parent, and the other returned. |
284 | */ |
285 | private Node appendNormalized( |
286 | Node pending, Node current, DocumentFragment parent) { |
287 | if (pending == null) { return current; } |
288 | if (pending.getNodeType() != Node.TEXT_NODE |
289 | || current.getNodeType() != Node.TEXT_NODE) { |
290 | parent.appendChild(pending); |
291 | return current; |
292 | } |
293 | Text a = (Text) pending, b = (Text) current; |
294 | Text combined = doc.createTextNode(a.getTextContent() + b.getTextContent()); |
295 | if (needsDebugData) { |
296 | Nodes.setFilePositionFor( |
297 | combined, |
298 | FilePosition.span( |
299 | Nodes.getFilePositionFor(a), |
300 | Nodes.getFilePositionFor(b))); |
301 | Nodes.setRawText(combined, Nodes.getRawText(a) + Nodes.getRawText(b)); |
302 | } |
303 | return combined; |
304 | } |
305 | |
306 | /** |
307 | * Records the fact that a tag has been seen, updating internal state |
308 | * |
309 | * @param start the token of the beginning of the tag, so {@code "<p"} for a |
310 | * paragraph start, {@code "</p"} for an end tag. |
311 | * @param end the token of the beginning of the tag, so {@code ">"} for a |
312 | * paragraph start, {@code "/>"} for an unary break tag. |
313 | * @param attrStubs the attributes for the element. |
314 | */ |
315 | public void processTag(Token<HtmlTokenType> start, Token<HtmlTokenType> end, |
316 | List<AttrStub> attrStubs) { |
317 | if (CajaTreeBuilder.DEBUG) { |
318 | System.err.println("processTag(" + start + ", " + end + ")"); |
319 | } |
320 | boolean isEndTag = CajaTreeBuilder.isEndTag(start.text); |
321 | String tagName = start.text.substring(isEndTag ? 2 : 1); |
322 | boolean isHtml = checkName(tagName); |
323 | if (isHtml) { tagName = Strings.toLowerCase(tagName); } |
324 | |
325 | HtmlAttributes htmlAttrs = new HtmlAttributes(AttributeName.HTML); |
326 | List<Attr> attrs = Lists.newArrayList(); |
327 | if (!attrStubs.isEmpty()) { |
328 | for (AttrStub as : attrStubs) { |
329 | String qname = as.nameTok.text; |
330 | Attr attrNode; |
331 | boolean isAttrHtml; |
332 | try { |
333 | String name; |
334 | if ("xmlns".equals(qname)) { |
335 | if (!Namespaces.HTML_NAMESPACE_URI.equals(as.value)) { |
336 | // We do not allow overriding of the default namespace when |
337 | // parsing HTML. |
338 | mq.addMessage( |
339 | MessageType.CANNOT_OVERRIDE_DEFAULT_NAMESPACE_IN_HTML, |
340 | as.nameTok.pos); |
341 | } |
342 | continue; |
343 | } else { |
344 | isAttrHtml = isHtml && checkName(qname); |
345 | if (isAttrHtml) { |
346 | name = Strings.toLowerCase(qname); |
347 | attrNode = maybeCreateAttributeNs(Namespaces.HTML_NAMESPACE_URI, |
348 | name, as); |
349 | if (attrNode == null) { |
350 | // Ignore this attribute. |
351 | continue; |
352 | } |
353 | } else { |
354 | name = AttributeNameFixup.fixupNameFromQname(qname); |
355 | attrNode = maybeCreateAttribute(name, as); |
356 | if (attrNode == null) { |
357 | // Ignore this attribute. |
358 | continue; |
359 | } |
360 | } |
361 | } |
362 | attrNode.setValue(as.value); |
363 | if (needsDebugData) { |
364 | Nodes.setFilePositionFor(attrNode, as.nameTok.pos); |
365 | Nodes.setFilePositionForValue(attrNode, as.valueTok.pos); |
366 | Nodes.setRawValue(attrNode, as.valueTok.text); |
367 | } |
368 | attrs.add(attrNode); |
369 | try { |
370 | htmlAttrs.addAttribute( |
371 | AttributeName.nameByString(name), |
372 | as.value, XmlViolationPolicy.ALLOW); |
373 | } catch (SAXException ex) { |
374 | if (CajaTreeBuilder.DEBUG) { ex.printStackTrace(); } |
375 | } |
376 | } catch (DOMException ex) { |
377 | ex.printStackTrace(); |
378 | mq.addMessage( |
379 | MessageType.INVALID_IDENTIFIER, MessageLevel.WARNING, |
380 | as.nameTok.pos, |
381 | MessagePart.Factory.valueOf(as.nameTok.text)); |
382 | } |
383 | } |
384 | } |
385 | ElementName elName = elNames.get(tagName); |
386 | if (elName == null) { |
387 | elName = ElementName.elementNameByString(tagName); |
388 | // Store element names because the underlying tree builder compares them |
389 | // using ==. |
390 | elNames.put(tagName, elName); |
391 | } |
392 | if (processingFirstTag && elName == ElementName.HTML && !isEndTag) { |
393 | // Indicate to fragment-retrieval code that the top-level |
394 | // <html> element came from the input, and wasn't synthesized |
395 | // by the underlying parser implementation. |
396 | topLevelHtmlFromInput = true; |
397 | } |
398 | processingFirstTag = false; |
399 | try { |
400 | if (builder.needsDebugData) { |
401 | if (isEndTag) { |
402 | // Version 1.2.1 of the TreeBuilder has a bug where it does not |
403 | // generate element popped events for body and head elements. |
404 | if (elName == ElementName.HTML) { |
405 | Token<HtmlTokenType> tok= Token.instance( |
406 | "", HtmlTokenType.TAGEND, FilePosition.startOf(start.pos)); |
407 | if (!builder.wasOpened("frameset")) { |
408 | builder.setTokenContext(tok, tok); |
409 | if (!builder.wasOpened("body")) { |
410 | if (!builder.wasOpened("head")) { |
411 | builder.startTag( |
412 | ElementName.HEAD, HtmlAttributes.EMPTY_ATTRIBUTES, false); |
413 | builder.endTag(ElementName.HEAD); |
414 | } |
415 | builder.headClosed(); |
416 | builder.startTag( |
417 | ElementName.BODY, HtmlAttributes.EMPTY_ATTRIBUTES, false); |
418 | builder.endTag(ElementName.BODY); |
419 | } |
420 | builder.bodyClosed(); |
421 | } |
422 | } |
423 | } |
424 | builder.setTokenContext(start, end); |
425 | } |
426 | if (isEndTag) { |
427 | builder.endTag(elName); |
428 | if (builder.needsDebugData) { |
429 | // Make sure that implicit body and head tag are marked as ending |
430 | // before the </html> tag. |
431 | if (elName == ElementName.BODY) { |
432 | builder.bodyClosed(); |
433 | } else if (elName == ElementName.HEAD) { |
434 | builder.headClosed(); |
435 | } |
436 | } |
437 | } else { |
438 | builder.startTag(elName, toHtmlAttributes(attrs, htmlAttrs), |
439 | end.text.equals("/>")); |
440 | } |
441 | } catch (SAXException ex) { |
442 | throw new SomethingWidgyHappenedError(ex); |
443 | } |
444 | } |
445 | |
446 | /** |
447 | * Adds the given comment node to the DOM. |
448 | */ |
449 | public void processComment(Token<HtmlTokenType> commentToken) { |
450 | String text = commentToken.text.substring( |
451 | "<!--".length(), commentToken.text.lastIndexOf("--")); |
452 | commentToken = Token.instance(text, commentToken.type, commentToken.pos); |
453 | char[] chars; |
454 | int n = text.length(); |
455 | if (n <= charBuf.length) { |
456 | chars = charBuf; |
457 | text.getChars(0, n, chars, 0); |
458 | } else { |
459 | chars = text.toCharArray(); |
460 | } |
461 | builder.setTokenContext(commentToken, commentToken); |
462 | try { |
463 | builder.comment(chars, 0, n); |
464 | } catch (SAXException ex) { |
465 | throw new RuntimeException(ex); |
466 | } |
467 | } |
468 | |
469 | private boolean checkName(String qname) { |
470 | if (qname.indexOf(':', 1) < 0) { |
471 | return true; |
472 | } else { |
473 | needsNamespaceFixup = true; |
474 | return false; |
475 | } |
476 | } |
477 | |
478 | private static final Map<HtmlAttributes, List<Attr>> HTML_ASSOCIATED_ATTRS |
479 | = new WeakHashMap<HtmlAttributes, List<Attr>>(); |
480 | private static HtmlAttributes toHtmlAttributes( |
481 | List<Attr> attrs, HtmlAttributes blank) { |
482 | HTML_ASSOCIATED_ATTRS.put(blank, attrs); |
483 | return blank; |
484 | } |
485 | |
486 | static List<Attr> getAssociatedAttrs(HtmlAttributes attrs) { |
487 | List<Attr> attrList = HTML_ASSOCIATED_ATTRS.get(attrs); |
488 | if (attrList == null) { attrList = Collections.emptyList(); } |
489 | return attrList; |
490 | } |
491 | |
492 | /** |
493 | * Adds the given text node to the DOM. |
494 | */ |
495 | public void processText(Token<HtmlTokenType> textToken) { |
496 | if (CajaTreeBuilder.DEBUG) { |
497 | System.err.println( |
498 | "processText(\"" |
499 | + textToken.text.replaceAll("\r\n?|\n", "\\n") + "\")"); |
500 | } |
501 | // htmlparser doesn't recognize \r as whitespace. |
502 | String text = textToken.text.replaceAll("\r\n?", "\n"); |
503 | if (textToken.type == HtmlTokenType.TEXT) { |
504 | text = fixBrokenEntities(text, textToken.pos); |
505 | } |
506 | if (text.equals(textToken.text)) { |
507 | textToken = Token.instance(text, textToken.type, textToken.pos); |
508 | } |
509 | char[] chars; |
510 | int n = text.length(); |
511 | if (n <= charBuf.length) { |
512 | chars = charBuf; |
513 | text.getChars(0, n, chars, 0); |
514 | } else { |
515 | chars = text.toCharArray(); |
516 | } |
517 | builder.setTokenContext(textToken, textToken); |
518 | try { |
519 | builder.characters(chars, 0, n); |
520 | } catch (SAXException ex) { |
521 | throw new SomethingWidgyHappenedError(ex); |
522 | } |
523 | } |
524 | |
525 | /** |
526 | * Matches possible HTML entities that lack a closing semicolon. |
527 | */ |
528 | private static final Pattern BROKEN_ENTITY = Pattern.compile( |
529 | "" |
530 | + "&(?:" |
531 | // A named entity. |
532 | + "[A-Za-z][0-9A-Za-z]{1,11}(?![;0-9A-Za-z])" |
533 | // A numeric entity. |
534 | + "|#(?:" |
535 | // A decimal entity |
536 | + "[0-9]{1,7}(?![;0-9])" |
537 | // A hexadecimal entity. |
538 | + "|[Xx][0-9A-Fa-f]{1,6}(?![;0-9A-Fa-f])" |
539 | + ")" |
540 | + ")" |
541 | ); |
542 | public String fixBrokenEntities(String rawText, FilePosition fp) { |
543 | int amp = rawText.indexOf('&'); |
544 | if (amp >= 0) { |
545 | Matcher m = BROKEN_ENTITY.matcher(rawText); |
546 | if (m.find(amp)) { |
547 | StringBuilder sb = new StringBuilder(rawText.length() + 16); |
548 | int pos = 0; |
549 | do { |
550 | String entity = m.group(); |
551 | if (entity.charAt(1) == '#' |
552 | || HtmlEntities.isEntityName(entity.substring(1))) { |
553 | sb.append(rawText, pos, m.end()).append(';'); |
554 | pos = m.end(); |
555 | if (needsDebugData) { |
556 | mq.addMessage( |
557 | MessageType.MALFORMED_HTML_ENTITY, fp, |
558 | MessagePart.Factory.valueOf(entity)); |
559 | } |
560 | } |
561 | } while (m.find()); |
562 | if (pos != 0) { |
563 | sb.append(rawText, pos, rawText.length()); |
564 | return sb.toString(); |
565 | } |
566 | } |
567 | } |
568 | return rawText; |
569 | } |
570 | |
571 | /** |
572 | * Creates a w3c dom attribute. |
573 | * |
574 | * @param attrName Attribute name. |
575 | * @param as The attribute stub. |
576 | * @return A w3c attribute if the attribute name is valid, null otherwise. |
577 | */ |
578 | public Attr maybeCreateAttribute(String attrName, AttrStub as) { |
579 | try { |
580 | return doc.createAttribute(attrName); |
581 | } catch (DOMException e) { |
582 | // Ignore DOMException's like INVALID_CHARACTER_ERR since its an html |
583 | // document. |
584 | mq.addMessage(DomParserMessageType.IGNORING_TOKEN, as.nameTok.pos, |
585 | MessagePart.Factory.valueOf("'" + as.nameTok.text + "'")); |
586 | logger.log(Level.FINE, "Ignoring DOMException in maybeCreateAttribute", |
587 | e); |
588 | return null; |
589 | } |
590 | } |
591 | |
592 | /** |
593 | * Creates a w3c dom attribute in the given namespace. |
594 | * |
595 | * @param nsUri The namespace uri to use. |
596 | * @param attrName Attribute name. |
597 | * @param as The attribute stub. |
598 | * @return A w3c attribute if the attribute name is valid, null otherwise. |
599 | */ |
600 | public Attr maybeCreateAttributeNs(String nsUri, String attrName, |
601 | AttrStub as) { |
602 | try { |
603 | return doc.createAttributeNS(nsUri, attrName); |
604 | } catch (DOMException e) { |
605 | // Ignore DOMException's like INVALID_CHARACTER_ERR since its an html |
606 | // document. |
607 | mq.addMessage(DomParserMessageType.IGNORING_TOKEN, as.nameTok.pos, |
608 | MessagePart.Factory.valueOf("'" + as.nameTok.text + "'")); |
609 | logger.log(Level.FINE, "Ignoring DOMException in maybeCreateAttributeNs", |
610 | e); |
611 | return null; |
612 | } |
613 | } |
614 | |
615 | // For testing. |
616 | Element builderRootElement() { |
617 | return builder.getRootElement(); |
618 | } |
619 | } |