diff --git a/qtism/data/content/xhtml/html5/Rb.php b/qtism/data/content/xhtml/html5/Rb.php new file mode 100644 index 000000000..72a50a8f6 --- /dev/null +++ b/qtism/data/content/xhtml/html5/Rb.php @@ -0,0 +1,35 @@ +getContent()->getArrayCopy(); } elseif ($component instanceof Figcaption) { return $component->getContent()->getArrayCopy(); + } elseif ($component instanceof Ruby) { + return $component->getContent()->getArrayCopy(); + } elseif ($component instanceof Rb) { + return $component->getContent()->getArrayCopy(); + } elseif ($component instanceof Rp) { + return $component->getContent()->getArrayCopy(); + } elseif ($component instanceof Rt) { + return $component->getContent()->getArrayCopy(); } } @@ -338,6 +354,10 @@ protected function getChildrenElements(DOMElement $element) return self::getChildElements($element); } elseif ($localName === 'simpleMatchSet') { return $this->getChildElementsByTagName($element, 'simpleAssociableChoice'); + } elseif ($localName === Figure::QTI_CLASS_NAME_FIGURE) { + return $this->getChildElementsByTagName($element, [Figcaption::QTI_CLASS_NAME_FIGCAPTION, Img::QTI_CLASS_NAME_IMG]); + } elseif ($localName === Ruby::QTI_CLASS_NAME) { + return $this->getChildElementsByTagName($element, [Rb::QTI_CLASS_NAME, Rp::QTI_CLASS_NAME, Rt::QTI_CLASS_NAME]); } elseif ($localName === 'gapImg') { return $this->getChildElementsByTagName($element, 'object'); } elseif ($element->localName === 'infoControl') { diff --git a/qtism/data/storage/xml/marshalling/MarshallerFactory.php b/qtism/data/storage/xml/marshalling/MarshallerFactory.php index 377b6ee27..1ef5068e5 100644 --- a/qtism/data/storage/xml/marshalling/MarshallerFactory.php +++ b/qtism/data/storage/xml/marshalling/MarshallerFactory.php @@ -421,7 +421,7 @@ public function createMarshaller($object, array $args = []) } } catch (ReflectionException $e) { $msg = "No marshaller implementation could be found for component '${qtiClassName}'."; - throw new RuntimeException($msg, 0, $e); + throw new MarshallerNotFoundException($msg, 0, $e); } $marshaller = $this->instantiateMarshaller($class, $args); diff --git a/qtism/data/storage/xml/marshalling/Qti22MarshallerFactory.php b/qtism/data/storage/xml/marshalling/Qti22MarshallerFactory.php index 6281e1c13..85a36a9b2 100644 --- a/qtism/data/storage/xml/marshalling/Qti22MarshallerFactory.php +++ b/qtism/data/storage/xml/marshalling/Qti22MarshallerFactory.php @@ -26,6 +26,10 @@ use qtism\common\utils\Reflection; use qtism\data\content\xhtml\html5\Figcaption; use qtism\data\content\xhtml\html5\Figure; +use qtism\data\content\xhtml\html5\Rb; +use qtism\data\content\xhtml\html5\Rp; +use qtism\data\content\xhtml\html5\Rt; +use qtism\data\content\xhtml\html5\Ruby; use ReflectionClass; /** @@ -42,6 +46,10 @@ public function __construct() parent::__construct(); $this->addMappingEntry(Figure::QTI_CLASS_NAME_FIGURE, Html5ContentMarshaller::class); $this->addMappingEntry(Figcaption::QTI_CLASS_NAME_FIGCAPTION, Html5ContentMarshaller::class); + $this->addMappingEntry(Ruby::QTI_CLASS_NAME, Html5ContentMarshaller::class); + $this->addMappingEntry(Rb::QTI_CLASS_NAME, Html5ContentMarshaller::class); + $this->addMappingEntry(Rp::QTI_CLASS_NAME, Html5ContentMarshaller::class); + $this->addMappingEntry(Rt::QTI_CLASS_NAME, Html5ContentMarshaller::class); } /** diff --git a/qtism/data/storage/xml/marshalling/RecursiveMarshaller.php b/qtism/data/storage/xml/marshalling/RecursiveMarshaller.php index 9c339aea1..76c6909a6 100644 --- a/qtism/data/storage/xml/marshalling/RecursiveMarshaller.php +++ b/qtism/data/storage/xml/marshalling/RecursiveMarshaller.php @@ -221,14 +221,16 @@ protected function marshall(QtiComponent $component) // Hierarchical node, 1st pass. $this->mark($node); $this->pushTrail($node); // repush for a further pass. - $children = array_reverse($this->getChildrenComponents($node)); // next nodes to explore. + $childrenComponentList = $this->getChildrenComponents($node) ?? []; + $children = array_reverse($childrenComponentList); // next nodes to explore. foreach ($children as $c) { $this->pushTrail($c); } - } elseif ($this->isMarked($node)) { + } elseif ($this->isMarked($node) && !$node instanceof DOMElement) { // Push the result on the trail. - $finals = $this->emptyFinal(count($this->getChildrenComponents($node))); + $childrenComponentList = $this->getChildrenComponents($node) ?? []; + $finals = $this->emptyFinal(count($childrenComponentList)); $marshaller = $this->getMarshallerFactory()->createMarshaller($node); $element = $marshaller->marshallChildrenKnown($node, $finals); $this->pushProcessed($element); diff --git a/qtism/runtime/rendering/markup/xhtml/XhtmlRenderingEngine.php b/qtism/runtime/rendering/markup/xhtml/XhtmlRenderingEngine.php index 1af27086e..42ec91fe5 100644 --- a/qtism/runtime/rendering/markup/xhtml/XhtmlRenderingEngine.php +++ b/qtism/runtime/rendering/markup/xhtml/XhtmlRenderingEngine.php @@ -23,6 +23,12 @@ namespace qtism\runtime\rendering\markup\xhtml; +use OAT\Library\QtiItemJsonCompilation\Rt; +use qtism\data\content\xhtml\html5\Figcaption; +use qtism\data\content\xhtml\html5\Figure; +use qtism\data\content\xhtml\html5\Rb; +use qtism\data\content\xhtml\html5\Rp; +use qtism\data\content\xhtml\html5\Ruby; use qtism\runtime\rendering\markup\AbstractMarkupRenderingEngine; /** @@ -154,6 +160,11 @@ public function __construct() $this->registerRenderer('positionObjectStage', new PositionObjectStageRenderer()); $this->registerRenderer('assessmentItem', new AssessmentItemRenderer()); $this->registerRenderer('printedVariable', new PrintedVariableRenderer()); + $this->registerRenderer(Figure::QTI_CLASS_NAME_FIGURE, new ExternalQtiComponentRenderer()); + $this->registerRenderer(Figcaption::QTI_CLASS_NAME_FIGCAPTION, new ExternalQtiComponentRenderer()); + $this->registerRenderer(Ruby::QTI_CLASS_NAME, new ExternalQtiComponentRenderer()); + $this->registerRenderer(Rb::QTI_CLASS_NAME, new ExternalQtiComponentRenderer()); + $this->registerRenderer(Rp::QTI_CLASS_NAME, new ExternalQtiComponentRenderer()); // External QTI Components. $this->registerRenderer('math', new MathRenderer()); diff --git a/test/qtismtest/data/content/xhtml/html5/Html5LayoutElementTest.php b/test/qtismtest/data/content/xhtml/html5/Html5LayoutElementTest.php index 8f0ed57f3..355fb981c 100644 --- a/test/qtismtest/data/content/xhtml/html5/Html5LayoutElementTest.php +++ b/test/qtismtest/data/content/xhtml/html5/Html5LayoutElementTest.php @@ -21,7 +21,9 @@ public function testCreateWithValues(): void $lang = 'en'; $label = 'This is the label.'; - $subject = new FakeHtml5LayoutElement($title, $role, $id, $class, $lang, $label); + $subject = $this->getMockForAbstractClass(Html5LayoutElement::class, [ + $title, $role, $id, $class, $lang, $label + ]); self::assertSame($title, $subject->getTitle()); self::assertEquals(Role::getConstantByName($role), $subject->getRole()); @@ -34,7 +36,7 @@ public function testCreateWithValues(): void public function testCreateWithDefaultValues(): void { - $subject = new FakeHtml5LayoutElement(); + $subject = $this->getMockForAbstractClass(Html5LayoutElement::class); self::assertSame('', $subject->getTitle()); self::assertSame('', $subject->getId()); @@ -46,7 +48,7 @@ public function testCreateWithDefaultValues(): void public function testSetContent(): void { - $subject = new FakeHtml5LayoutElement(); + $subject = $this->getMockForAbstractClass(Html5LayoutElement::class); $content = new FlowCollection( [ new P(), @@ -61,11 +63,3 @@ public function testSetContent(): void self::assertEquals($content, $subject->getComponents()); } } - -class FakeHtml5LayoutElement extends Html5LayoutElement -{ - public function getQtiClassName(): string - { - return ''; - } -} diff --git a/test/qtismtest/data/content/xhtml/html5/RbTest.php b/test/qtismtest/data/content/xhtml/html5/RbTest.php new file mode 100644 index 000000000..923b1780c --- /dev/null +++ b/test/qtismtest/data/content/xhtml/html5/RbTest.php @@ -0,0 +1,57 @@ +getId()); + self::assertEquals($class, $subject->getClass()); + } + + public function testCreateWithDefaultValues(): void + { + $subject = new Rb(); + + self::assertEquals('', $subject->getId()); + self::assertEquals('', $subject->getClass()); + } + + public function testGetQtiClassName(): void + { + $subject = new Rb(); + + self::assertEquals(self::SUBJECT_QTI_CLASS, $subject->getQtiClassName()); + } +} diff --git a/test/qtismtest/data/content/xhtml/html5/RpTest.php b/test/qtismtest/data/content/xhtml/html5/RpTest.php new file mode 100644 index 000000000..e548e92cc --- /dev/null +++ b/test/qtismtest/data/content/xhtml/html5/RpTest.php @@ -0,0 +1,57 @@ +getId()); + self::assertEquals($class, $subject->getClass()); + } + + public function testCreateWithDefaultValues(): void + { + $subject = new Rp(); + + self::assertEquals('', $subject->getId()); + self::assertEquals('', $subject->getClass()); + } + + public function testGetQtiClassName(): void + { + $subject = new Rp(); + + self::assertEquals(self::SUBJECT_QTI_CLASS, $subject->getQtiClassName()); + } +} diff --git a/test/qtismtest/data/content/xhtml/html5/RtTest.php b/test/qtismtest/data/content/xhtml/html5/RtTest.php new file mode 100644 index 000000000..c6e034165 --- /dev/null +++ b/test/qtismtest/data/content/xhtml/html5/RtTest.php @@ -0,0 +1,57 @@ +getId()); + self::assertEquals($class, $subject->getClass()); + } + + public function testCreateWithDefaultValues(): void + { + $subject = new Rt(); + + self::assertEquals('', $subject->getId()); + self::assertEquals('', $subject->getClass()); + } + + public function testGetQtiClassName(): void + { + $subject = new Rt(); + + self::assertEquals(self::SUBJECT_QTI_CLASS, $subject->getQtiClassName()); + } +} diff --git a/test/qtismtest/data/content/xhtml/html5/RubyTest.php b/test/qtismtest/data/content/xhtml/html5/RubyTest.php new file mode 100644 index 000000000..3226b331e --- /dev/null +++ b/test/qtismtest/data/content/xhtml/html5/RubyTest.php @@ -0,0 +1,57 @@ +getId()); + self::assertEquals($class, $subject->getClass()); + } + + public function testCreateWithDefaultValues(): void + { + $subject = new Ruby(); + + self::assertEquals('', $subject->getId()); + self::assertEquals('', $subject->getClass()); + } + + public function testGetQtiClassName(): void + { + $subject = new Ruby(); + + self::assertEquals(self::SUBJECT_QTI_CLASS, $subject->getQtiClassName()); + } +} diff --git a/test/qtismtest/data/storage/xml/marshalling/Html5ElementMarshallerTest.php b/test/qtismtest/data/storage/xml/marshalling/Html5ElementMarshallerTest.php new file mode 100644 index 000000000..88626032a --- /dev/null +++ b/test/qtismtest/data/storage/xml/marshalling/Html5ElementMarshallerTest.php @@ -0,0 +1,300 @@ +', + $this->namespaceTag(Figure::QTI_CLASS_NAME_FIGURE), + $id, + $class, + $lang, + $label, + $title, + $role + ); + + $html5Element = new Figure($title, Role::getConstantByName($role), $id, $class, $lang, $label); + + $this->assertMarshalling($expected, $html5Element); + } + + /** + * @throws MarshallerNotFoundException + * @throws MarshallingException + */ + public function testMarshall22WithDefaultValues(): void + { + $expected = '<' . $this->namespaceTag(Figure::QTI_CLASS_NAME_FIGURE) . '/>'; + + $html5Element = new Figure(); + + $this->assertMarshalling($expected, $html5Element); + } + + /** + * @throws MarshallerNotFoundException + */ + public function testUnmarshall22(): void + { + $title = 'the title'; + $role = 'note'; + $id = 'Identifier'; + $class = 'a css class'; + $lang = 'es'; + $label = 'A label'; + + $xml = sprintf( + '<%s id="%s" class="%s" xml:lang="%s" label="%s" title="%s" role="%s"/>', + $this->namespaceTag(Figure::QTI_CLASS_NAME_FIGURE), + $id, + $class, + $lang, + $label, + $title, + $role + ); + + $expected = new Figure($title, Role::getConstantByName($role), $id, $class, $lang, $label); + + $this->assertUnmarshalling($expected, $xml); + } + + /** + * @throws MarshallerNotFoundException + */ + public function testUnmarshall22WithDefaultValues(): void + { + $xml = '<' . $this->namespaceTag(Figure::QTI_CLASS_NAME_FIGURE) . '/>'; + + $expected = new Figure(); + + $this->assertUnmarshalling($expected, $xml); + } + + public function testRubyMarshaller() + { + $id = 'id'; + $class = 'testclass'; + + $expected = sprintf( + '<%1$s id="%2$s" class="%3$s"><%4$s>真<%5$s>まこと<%6$s>真', + $this->namespaceTag(Ruby::QTI_CLASS_NAME), + $id, + $class, + $this->prefixTag(Rt::QTI_CLASS_NAME), + $this->prefixTag(Rb::QTI_CLASS_NAME), + $this->prefixTag(Rp::QTI_CLASS_NAME), + $this->prefixTag(Ruby::QTI_CLASS_NAME) + ); + + $rb = new Rb(); + $rb->setContent(new FlowCollection([new TextRun('まこと')])); + + $rt = new Rt(); + $rt->setContent(new FlowCollection([new TextRun('真')])); + + $rp = new Rp(); + $rp->setContent(new FlowCollection([new TextRun('真')])); + + $object = new Ruby(null, null, $id, $class); + $object->setContent(new FlowCollection([ $rt, $rb, $rp])); + + $this->assertMarshalling($expected, $object); + } + /** + * @throws MarshallerNotFoundException + * @throws MarshallingException + */ + public function testRubyMarshall22WithDefaultValues(): void + { + $expected = sprintf( + '<%s/>', + $this->namespaceTag(Ruby::QTI_CLASS_NAME) + ); + + $ruby = new Ruby(); + + $this->assertMarshalling($expected, $ruby); + } + + /** + * @throws MarshallerNotFoundException + */ + public function testRubyUnMarshallerDoesNotExistInQti21(): void + { + $this->assertHtml5UnmarshallingOnlyInQti22AndAbove( + sprintf( + '<%s>', + $this->namespaceTag(Ruby::QTI_CLASS_NAME), + $this->prefixTag(Ruby::QTI_CLASS_NAME) + ), + Ruby::QTI_CLASS_NAME + ); + } + + /** + * @throws MarshallerNotFoundException + */ + public function testRubyUnmarshall22(): void + { + $id = 'id'; + $class = 'testclass'; + + $xml = sprintf( + '<%1$s id="%2$s" class="%3$s">', + $this->namespaceTag(Ruby::QTI_CLASS_NAME), + $id, + $class, + $this->prefixTag(Ruby::QTI_CLASS_NAME) + ); + + $expected = new Ruby(null, null, $id, $class); + + $this->assertUnmarshalling($expected, $xml); + } + + public function testRubyUnmarshall22WithDefaultValues(): void + { + $xml = sprintf( + '<%s>', + $this->namespaceTag(Ruby::QTI_CLASS_NAME), + $this->prefixTag(Ruby::QTI_CLASS_NAME) + ); + + $expected = new Ruby(); + + $this->assertUnmarshalling($expected, $xml); + } + + /** + * @param Html5Element $object + * @param string $elementName + * @throws MarshallerNotFoundException + * @throws MarshallingException + */ + protected function assertHtml5MarshallingOnlyInQti22AndAbove(Html5Element $object, string $elementName): void + { + $this->expectException(MarshallerNotFoundException::class); + $this->expectExceptionMessage('No mapping entry found for QTI class name \'' . $elementName . '\'.'); + $marshaller = $this->getMarshallerFactory('2.1.0')->createMarshaller($object); + $marshaller->marshall($object); + } + + /** + * @param string $xml + * @param string $elementName + * @throws MarshallerNotFoundException + */ + public function assertHtml5UnmarshallingOnlyInQti22AndAbove(string $xml, string $elementName): void + { + $element = $this->createDOMElement($xml); + $this->expectException(MarshallerNotFoundException::class); + $this->expectExceptionMessage(sprintf( + "No marshaller implementation could be found for component '%s'.", + $elementName + )); + $marshaller = $this->getMarshallerFactory('2.1.0')->createMarshaller($element); + $marshaller->unmarshall($element); + } + + /** + * @param string $expected + * @param Html5Element $object + * @param Marshaller|null $marshaller Optional marshaller to use for marshalling he object. + * @throws MarshallerNotFoundException + * @throws MarshallingException + */ + protected function assertMarshalling(string $expected, Html5Element $object, Marshaller $marshaller = null): void + { + if ($marshaller === null) { + $marshaller = $this->getMarshallerFactory('2.2.0')->createMarshaller($object); + } + $element = $marshaller->marshall($object); + + $dom = new DOMDocument('1.0', 'UTF-8'); + $element = $dom->importNode($element, true); + $this->assertEquals($expected, $dom->saveXML($element)); + } + + /** + * @param Html5Element $expected + * @param string $xml + * @param Marshaller|null $marshaller Optional marshaller to use for marshalling he object. + * @throws MarshallerNotFoundException + */ + protected function assertUnmarshalling(Html5Element $expected, string $xml, Marshaller $marshaller = null): void + { + $element = $this->createDOMElement($xml); + + if ($marshaller === null) { + $marshaller = $this->getMarshallerFactory('2.2.0')->createMarshaller($element); + } + + $component = $marshaller->unmarshall($element); + $this::assertEquals($expected, $component); + } + + /** + * @param string $xml + * @param string $exception + * @param string $message + * @throws MarshallerNotFoundException + */ + public function assertUnmarshallingException(string $xml, string $exception, string $message): void + { + $element = $this->createDOMElement($xml); + $marshaller = $this->getMarshallerFactory('2.2.0')->createMarshaller($element); + + $this->expectException($exception); + $this->expectExceptionMessage($message); + + $marshaller->unmarshall($element); + } + + protected function namespaceTag(string $tagName): string + { + return $this->prefixTag($tagName) . ' xmlns:' . self::HTML5_PREFIX . '="' . self::HTML5_NAMESPACE . '"'; + } + + protected function prefixTag(string $tagName): string + { + return self::HTML5_PREFIX . ':' . $tagName; + } +} diff --git a/test/qtismtest/data/storage/xml/marshalling/Html5LayoutMarshallerTest.php b/test/qtismtest/data/storage/xml/marshalling/Html5LayoutMarshallerTest.php index 80e2bea36..24c90f6f3 100644 --- a/test/qtismtest/data/storage/xml/marshalling/Html5LayoutMarshallerTest.php +++ b/test/qtismtest/data/storage/xml/marshalling/Html5LayoutMarshallerTest.php @@ -3,6 +3,7 @@ namespace qtismtest\data\storage\xml\marshalling; use DOMDocument; +use qtism\data\storage\xml\marshalling\MarshallerNotFoundException; use \RuntimeException; use qtism\data\content\FlowCollection; use qtism\data\content\TextRun; @@ -78,7 +79,7 @@ public function testMarshallerBelow2p2Fails(): void $figure = new Figure(); $figure->setContent(new FlowCollection([$figCaption])); - $this->expectException(RuntimeException::class); + $this->expectException(MarshallerNotFoundException::class); $this->expectExceptionMessage( "No marshaller implementation could be found for component 'figure'." ); diff --git a/test/qtismtest/data/storage/xml/marshalling/MarshallerTest.php b/test/qtismtest/data/storage/xml/marshalling/MarshallerTest.php index 039259ec9..c987a1f64 100644 --- a/test/qtismtest/data/storage/xml/marshalling/MarshallerTest.php +++ b/test/qtismtest/data/storage/xml/marshalling/MarshallerTest.php @@ -7,13 +7,11 @@ use qtism\common\enums\BaseType; use qtism\data\expressions\BaseValue; use qtism\data\ItemSessionControl; -use qtism\data\QtiComponent; use qtism\data\storage\xml\marshalling\ItemSessionControlMarshaller; use qtism\data\storage\xml\marshalling\Marshaller; use qtismtest\QtiSmTestCase; use ReflectionClass; use RuntimeException; -use stdClass; /** * Class MarshallerTest @@ -87,7 +85,7 @@ public function testGetChildElementsByTagName() // We should find only 2 direct child elements. $dom->loadXML(''); $element = $dom->documentElement; - $marshaller = new FakeMarshaller('2.1.0'); + $marshaller = $this->getMockForAbstractClass(Marshaller::class, ['2.1.0']); $this::assertCount(2, $marshaller->getChildElementsByTagName($element, 'child')); } @@ -97,7 +95,7 @@ public function testGetChildElementsByTagNameMultiple() $dom = new DOMDocument('1.0', 'UTF-8'); $dom->loadXML(''); $element = $dom->documentElement; - $marshaller = new FakeMarshaller('2.1.0'); + $marshaller = $this->getMockForAbstractClass(Marshaller::class, ['2.1.0']); $this::assertCount(3, $marshaller->getChildElementsByTagName($element, ['child', 'grandChild'])); } @@ -110,7 +108,7 @@ public function testGetChildElementsByTagNameEmpty() // should be found. $dom->loadXML(''); $element = $dom->documentElement; - $marshaller = new FakeMarshaller('2.1.0'); + $marshaller = $this->getMockForAbstractClass(Marshaller::class, ['2.1.0']); $this::assertCount(0, $marshaller->getChildElementsByTagName($element, 'child')); } @@ -178,32 +176,3 @@ public function testNoSuchMagicMethod() $marshaller->hello(); } } - - - - - - -class FakeMarshaller extends Marshaller -{ - /** - * @inheritDoc - */ - protected function marshall(QtiComponent $component) - { - } - - /** - * @inheritDoc - */ - protected function unmarshall(DOMElement $element) - { - } - - /** - * @inheritDoc - */ - public function getExpectedQtiClassName() - { - } -} diff --git a/test/qtismtest/runtime/expressions/VariableProcessorTest.php b/test/qtismtest/runtime/expressions/VariableProcessorTest.php index 3b69068a2..8f8ca62b3 100644 --- a/test/qtismtest/runtime/expressions/VariableProcessorTest.php +++ b/test/qtismtest/runtime/expressions/VariableProcessorTest.php @@ -105,12 +105,12 @@ public function testWeighted() $variableExpr = $this->createComponentFromXml(''); $variableProcessor->setExpression($variableExpr); $result = $variableProcessor->process(); - $this::assertEquals(11.11, $result[0]->getValue()); - $this::assertEquals(13.31, $result[1]->getValue()); + $this::assertEquals(11.11, round($result[0]->getValue(), 2)); + $this::assertEquals(13.31, round($result[1]->getValue(), 2)); // The value in the state must be unchanged. $stateVal = $assessmentTestSession['Q01.var2']; - $this::assertEquals(10.1, $stateVal[0]->getValue()); - $this::assertEquals(12.1, $stateVal[1]->getValue()); + $this::assertEquals(10.1, round($stateVal[0]->getValue(), 1)); + $this::assertEquals(12.1, round($stateVal[1]->getValue(), 1)); } public function testMultipleOccurences() diff --git a/test/samples/custom/items/2_2/ruby_html5.xml b/test/samples/custom/items/2_2/ruby_html5.xml new file mode 100644 index 000000000..f8097d298 --- /dev/null +++ b/test/samples/custom/items/2_2/ruby_html5.xml @@ -0,0 +1,49 @@ + + + + + + + + + + 0 + + + +
+
+

村田

+ + + まこと + +

の出身地はどこですか

+
+
+ + 選びなさい + + + 北海道 + ほっかいどう + + + 東北 + 北陸 + 関東 + 甲信越 + 近畿 + 関西 + 四国 + 中国 + 九州 + +
+ +
\ No newline at end of file