Skip to content

Commit 0cfe667

Browse files
authored
Mayaaで名前空間を扱うためHTMLエレメント名として妥当でないものもパースできる (#113)
1 parent 3be8174 commit 0cfe667

4 files changed

Lines changed: 110 additions & 42 deletions

File tree

src-impl/org/seasar/mayaa/impl/builder/BuilderUtil.java

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -134,19 +134,23 @@ static PrefixMapping getDefaultPrefixMapping() {
134134
}
135135

136136
public static PrefixAwareName parseName(SpecificationNode node, String qName) {
137-
String[] parsed = qName.split(":");
138-
String prefix = null;
139-
String localName = null;
140-
URI namespaceURI = null;
137+
String prefix = "";
138+
String localName = qName;
139+
URI namespaceURI = node.getQName().getNamespaceURI();
141140
PrefixMapping mapping = null;
142-
if (parsed.length == 2) {
143-
prefix = parsed[0];
144-
localName = parsed[1];
145-
if (prefix.isEmpty()) {
146-
// :attrName のようにプレフィックス部分が空の時は":"も属性名に含める。
147-
// 本来のXML仕様としては許容されないがHTML仕様としては名前空間自体をサポートしていないため属性名として有効なものとした。
148-
localName = ":" + localName;
149-
}
141+
142+
int firstColonIndex = qName.indexOf(':');
143+
if (firstColonIndex == -1) {
144+
// コロンが存在しない場合はプレフィックスを空文字列にする。
145+
} else if (firstColonIndex == 0) {
146+
// コロンが先頭の文字の場合はプレフィックスを空文字列にする。コロンも含めてlocalNameとする。
147+
// 本来のXML仕様としては許容されないがHTML仕様としては名前空間自体をサポートしていないため属性名として有効なものとした。
148+
} else if (firstColonIndex == qName.length() - 1) {
149+
// コロンが末尾にある場合はプレフィックスを空文字列にする。コロンも含めてlocalNameとする。
150+
} else {
151+
prefix = qName.substring(0, firstColonIndex);
152+
localName = qName.substring(firstColonIndex + 1);
153+
150154
mapping = node.getMappingFromPrefix(prefix, true);
151155
if (mapping == null) {
152156
if ("xml".equals(prefix)) {
@@ -159,24 +163,11 @@ public static PrefixAwareName parseName(SpecificationNode node, String qName) {
159163
}
160164
}
161165
namespaceURI = mapping.getNamespaceURI();
162-
} else if (parsed.length == 1) {
163-
localName = parsed[0];
164-
namespaceURI = node.getQName().getNamespaceURI();
165-
166-
if (namespaceURI == null) {
167-
mapping = node.getMappingFromPrefix("", true);
168-
if (mapping == null) {
169-
mapping = getDefaultPrefixMapping();
170-
}
171-
namespaceURI = mapping.getNamespaceURI();
172-
}
173-
} else {
174-
throw new IllegalNameException(qName);
175166
}
176-
if (mapping != null) {
177-
prefix = mapping.getPrefix();
178-
} else {
179-
prefix = "";
167+
168+
if (namespaceURI == null) {
169+
mapping = getDefaultPrefixMapping();
170+
namespaceURI = mapping.getNamespaceURI();
180171
}
181172
PrefixAwareName ret = SpecificationUtil.createPrefixAwareName(
182173
SpecificationUtil.createQName(namespaceURI, localName),

src-impl/org/seasar/mayaa/impl/builder/SpecificationNodeHandler.java

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -217,24 +217,39 @@ protected SpecificationNode addNode(QName qName) {
217217
return addNode(qName, null, -1);
218218
}
219219

220+
/**
221+
* エレメント名のうち最初に見つかったコロンまでは、**名前空間プレフィックス**として評価が試みられます。
222+
* ただし、そのエレメント自身、もしくは先祖エレメントで `xmlns:{プレフィックス}="{名前空間URI}"` の形式による名前空間の定義が存在しない場合は、
223+
* コロンを含む**元の文字列全体をローカル名**として扱います(名前空間の解決を行いません)。
224+
* エレメント名が **コロンで終わる** 場合、名前空間プレフィックスとはみなされず、**全体がローカル名**として扱われます。
225+
*
226+
* @param qName
227+
* @param namespace
228+
* @return
229+
*/
220230
protected SpecificationNode addNode(String qName, Namespace namespace) {
221-
QName nodeQName;
222-
String prefix = null;
223-
224-
URI namespaceURI = URIImpl.NULL_NS_URI;
225-
int colonIndex = qName.indexOf(':');
226-
if (colonIndex != -1) {
227-
prefix = qName.substring(0, colonIndex);
228-
String localName = qName.substring(colonIndex+1);
231+
String prefix = "";
232+
String localName = qName;
233+
URI namespaceURI = namespace.getDefaultNamespaceURI();
234+
235+
int firstColonIndex = qName.indexOf(':');
236+
if (firstColonIndex == -1) {
237+
// コロンが存在しない場合はプレフィックスを空文字列にする。
238+
} else if (firstColonIndex == 0) {
239+
// コロンが先頭の文字の場合はプレフィックスを空文字列にする。コロンも含めてlocalNameとする。
240+
// 本来のXML仕様としては許容されないがHTML仕様としては名前空間自体をサポートしていないため属性名として有効なものとした。
241+
} else if (firstColonIndex == qName.length() - 1) {
242+
// コロンが末尾にある場合はプレフィックスを空文字列にする。コロンも含めてlocalNameとする。
243+
} else {
244+
prefix = qName.substring(0, firstColonIndex);
245+
localName = qName.substring(firstColonIndex + 1);
246+
229247
PrefixMapping mapping = namespace.getMappingFromPrefix(prefix, true);
230248
if (mapping != null) {
231249
namespaceURI = mapping.getNamespaceURI();
232250
}
233-
nodeQName = QNameImpl.getInstance(namespaceURI, localName);
234-
} else {
235-
nodeQName = QNameImpl.getInstance(namespace.getDefaultNamespaceURI(), qName);
236251
}
237-
252+
QName nodeQName = QNameImpl.getInstance(namespaceURI, localName);
238253
return addNode(nodeQName, prefix, -1);
239254
}
240255

src-impl/org/seasar/mayaa/impl/builder/parser/HtmlStandardScanner.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1607,6 +1607,7 @@ void runTokenizer(TokenHandler handler) {
16071607
handler.reportError("invalid-first-character-of-tag-name", null);
16081608
tokenizeState = TokenizeState.Data;
16091609
appendTextNode('<');
1610+
pushBack();
16101611
}
16111612
break;
16121613

@@ -1628,7 +1629,7 @@ void runTokenizer(TokenHandler handler) {
16281629
} else {
16291630
handler.reportError("invalid-first-character-of-tag-name", null);
16301631
tokenizeState = TokenizeState.BogusComment;
1631-
appendTextNode('<');
1632+
pushBack();
16321633
}
16331634
break;
16341635

src/test/java/org/seasar/mayaa/functional/engine/ParserTest.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,67 @@ class NotValidHtml {
541541

542542
execAndVerify("/target.html", useNewParser ? "/expected.html": "/expected-neko.html", null);
543543
}
544+
545+
@ParameterizedTest(name = "useNewParser {0} / TagBalance {1}")
546+
@CsvSource({"true, true", "true, false", "false, true", "false, false"})
547+
public void エレメント名がコロンで終わったら名前空間Prefixをローカルネームとして扱う(boolean useNewParser, boolean tagBalance) throws IOException {
548+
enableDump();
549+
setUseNewParser(useNewParser);
550+
setBalanceTag(tagBalance);
551+
DynamicRegisteredSourceHolder.registerContents("/target.html",
552+
"<!DOCTYPE html>\n" +
553+
"<html>\n" +
554+
"<body><section: param=a><p>text</p></section:></body>\n" +
555+
"</html>"
556+
);
557+
DynamicRegisteredSourceHolder.registerContents("/expected.html",
558+
"<!DOCTYPE html>\n" +
559+
"<html>\n" +
560+
"<body><section: param=\"a\"><p>text</p></section:></body>\n" +
561+
"</html>"
562+
);
563+
execAndVerify("/target.html", "/expected.html", null);
564+
}
565+
566+
/**
567+
* エレメント名の先頭文字として許されない文字がある場合は次の開始タグまではテキストノードとして扱われるが
568+
* 閉じタグの先頭文字として許されない文字の場合はHTMLコメントとして出力する。ブラウザでの解釈も同じ。
569+
* ただし、NekoHtmlの場合は不正な閉じタグのHTMLコメント出力はしない。
570+
*
571+
* @param useNewParser
572+
* @param tagBalance
573+
* @throws IOException
574+
* @see https://html.spec.whatwg.org/multipage/parsing.html#parse-error-invalid-first-character-of-tag-name
575+
*/
576+
@CsvSource({"true, true", "true, false", "false, true", "false, false"})
577+
@ParameterizedTest(name = "useNewParser {0} / TagBalance {1}")
578+
public void エレメント名がコロンで始まるときはエレメントとして評価されずテキストノードとして扱う_閉じタグはコメント化(boolean useNewParser, boolean tagBalance) throws IOException {
579+
setUseNewParser(useNewParser);
580+
setBalanceTag(tagBalance);
581+
enableDump();
582+
DynamicRegisteredSourceHolder.registerContents("/target.html",
583+
"<!DOCTYPE html>\n" +
584+
"<html>\n" +
585+
"<body><:div param=a><p>text</p></:div></body>\n" +
586+
"</html>"
587+
);
588+
DynamicRegisteredSourceHolder.registerContents("/expected-neko.html",
589+
"<!DOCTYPE html>\n" +
590+
"<html>\n" +
591+
"<body><:div param=a><p>text</p></body>\n" +
592+
"</html>"
593+
);
594+
// ウェブブラウザの解釈と同じ(エレメント名として不正な閉じタグはコメントとして付加される)
595+
DynamicRegisteredSourceHolder.registerContents("/expected.html",
596+
"<!DOCTYPE html>\n" +
597+
"<html>\n" +
598+
"<body><:div param=a><p>text</p><!--:div--></body>\n" +
599+
"</html>"
600+
);
601+
execAndVerify("/target.html", useNewParser ? "/expected.html": "/expected-neko.html", null);
602+
603+
}
604+
544605
}
545606

546607
@ParameterizedTest(name = "useNewParser {0}")

0 commit comments

Comments
 (0)