/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifdef MACOSX #define __ASSERT_MACROS_DEFINE_VERSIONS_WITHOUT_UNDERSCORES 0 #include #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include class Test : public SwModelTestBase { public: Test() : SwModelTestBase("/sw/qa/extras/ooxmlimport/data/", "Office Open XML Text") { } }; DECLARE_OOXMLIMPORT_TEST(testTdf108545_embeddedDocxIcon, "tdf108545_embeddedDocxIcon.docx") { uno::Reference xSupplier(getShape(1), uno::UNO_QUERY); CPPUNIT_ASSERT_EQUAL(embed::Aspects::MSOLE_ICON, xSupplier->getAspect()); } DECLARE_OOXMLIMPORT_TEST(testTdf121203, "tdf121203.docx") { // We imported the date field SwXTextDocument* pTextDoc = dynamic_cast(mxComponent.get()); CPPUNIT_ASSERT(pTextDoc); SwDoc* pDoc = pTextDoc->GetDocShell()->GetDoc(); IDocumentMarkAccess* pMarkAccess = pDoc->getIDocumentMarkAccess(); CPPUNIT_ASSERT_EQUAL(sal_Int32(1), pMarkAccess->getAllMarksCount()); // Custom sdt date content is imported correctly ::sw::mark::IDateFieldmark* pFieldmark = dynamic_cast<::sw::mark::IDateFieldmark*>(*pMarkAccess->getAllMarksBegin()); CPPUNIT_ASSERT(pFieldmark); CPPUNIT_ASSERT_EQUAL(OUString(ODF_FORMDATE), pFieldmark->GetFieldname()); const sw::mark::IFieldmark::parameter_map_t* const pParameters = pFieldmark->GetParameters(); OUString sDateFormat; auto pResult = pParameters->find(ODF_FORMDATE_DATEFORMAT); if (pResult != pParameters->end()) { pResult->second >>= sDateFormat; } OUString sLang; pResult = pParameters->find(ODF_FORMDATE_DATEFORMAT_LANGUAGE); if (pResult != pParameters->end()) { pResult->second >>= sLang; } OUString sCurrentDate = pFieldmark->GetContent(); CPPUNIT_ASSERT_EQUAL(OUString("dd-MMM-yy"), sDateFormat); CPPUNIT_ASSERT_EQUAL(OUString("en-GB"), sLang); CPPUNIT_ASSERT_EQUAL(OUString("17-Oct-2018 09:00"), sCurrentDate); } DECLARE_OOXMLIMPORT_TEST(testTdf109053, "tdf109053.docx") { // Table was imported into a text frame which led to a one page document // Originally the table takes two pages, so Writer should import it accordingly. CPPUNIT_ASSERT_EQUAL(2, getPages()); } DECLARE_OOXMLIMPORT_TEST(testTdf121664, "tdf121664.docx") { uno::Reference xLineNumbering(mxComponent, uno::UNO_QUERY); CPPUNIT_ASSERT(xLineNumbering.is()); // Without the accompanying fix in place, numbering did not restart on the // second page. CPPUNIT_ASSERT( getProperty(xLineNumbering->getLineNumberingProperties(), "RestartAtEachPage")); } DECLARE_OOXMLIMPORT_TEST(testTdf108849, "tdf108849.docx") { // sectPr element that is child element of body must be the last child. However, Word accepts it // in wrong places, and we should do the same (bug-to-bug compatibility) without creating extra sections. CPPUNIT_ASSERT_EQUAL(2, getParagraphs()); CPPUNIT_ASSERT_EQUAL_MESSAGE("Misplaced body-level sectPr's create extra sections!", 2, getPages()); } DECLARE_OOXMLIMPORT_TEST(testTdf97038, "tdf97038.docx") { // Without the accompanying fix in place, this test would have failed, as the importer lost the // fLayoutInCell shape property for wrap-though shapes. CPPUNIT_ASSERT(getProperty(getShapeByName("Kep2"), "IsFollowingTextFlow")); } DECLARE_OOXMLIMPORT_TEST(testTdf114212, "tdf114212.docx") { // Without the accompanying fix in place, this test would have failed with: // - Expected: 1427 // - Actual : 387 OUString aTop = parseDump("//fly[1]/infos/bounds", "top"); CPPUNIT_ASSERT_EQUAL(OUString("1427"), aTop); } DECLARE_OOXMLIMPORT_TEST(testTdf109524, "tdf109524.docx") { uno::Reference xTablesSupplier(mxComponent, uno::UNO_QUERY); uno::Reference xTables(xTablesSupplier->getTextTables(), uno::UNO_QUERY); // The table should have a small width (just to hold the short text in its single cell). // Until it's correctly implemented, we assign it 100% relative width. // Previously, the table (without explicitly set width) had huge actual width // and extended far outside of page's right border. CPPUNIT_ASSERT_EQUAL(true, getProperty(xTables->getByIndex(0), "IsWidthRelative")); CPPUNIT_ASSERT_EQUAL(sal_Int16(100), getProperty(xTables->getByIndex(0), "RelativeWidth")); } DECLARE_OOXMLIMPORT_TEST(testGroupShapeFontName, "groupshape-fontname.docx") { // Font names inside a group shape were not imported uno::Reference xGroup(getShape(1), uno::UNO_QUERY); uno::Reference xText = uno::Reference(xGroup->getByIndex(1), uno::UNO_QUERY_THROW)->getText(); CPPUNIT_ASSERT_EQUAL( OUString("Calibri"), getProperty(getRun(getParagraphOfText(1, xText), 1), "CharFontName")); CPPUNIT_ASSERT_EQUAL( OUString("Calibri"), getProperty(getRun(getParagraphOfText(1, xText), 1), "CharFontNameComplex")); CPPUNIT_ASSERT_EQUAL( OUString(""), getProperty(getRun(getParagraphOfText(1, xText), 1), "CharFontNameAsian")); } DECLARE_OOXMLIMPORT_TEST(testTdf124600, "tdf124600.docx") { uno::Reference xShape = getShape(1); // Without the accompanying fix in place, this test would have failed with: // - Expected: 0 // - Actual : 318 // i.e. the shape had an unexpected left margin, but not in Word. CPPUNIT_ASSERT_EQUAL(static_cast(0), getProperty(xShape, "HoriOrientPosition")); // Make sure that "Shape 1 text" (anchored in the header) has the same left margin as the body // text. OUString aShapeTextLeft = parseDump("/root/page/header/txt/anchored/fly/infos/bounds", "left"); OUString aBodyTextLeft = parseDump("/root/page/body/txt/infos/bounds", "left"); // Without the accompanying fix in place, this test would have failed with: // - Expected: 1701 // - Actual : 1815 // i.e. there was a >0 left margin on the text of the shape, resulting in incorrect horizontal // position. CPPUNIT_ASSERT_EQUAL(aBodyTextLeft, aShapeTextLeft); } DECLARE_OOXMLIMPORT_TEST(testTdf120548, "tdf120548.docx") { // Without the accompanying fix in place, this test would have failed with 'Expected: 00ff0000; // Actual: ffffffff', i.e. the numbering portion was black, not red. CPPUNIT_ASSERT_EQUAL(OUString("00ff0000"), parseDump("//Special[@nType='PortionType::Number']/SwFont", "color")); } DECLARE_OOXMLIMPORT_TEST(test120551, "tdf120551.docx") { auto nHoriOrientPosition = getProperty(getShape(1), "HoriOrientPosition"); // Without the accompanying fix in place, this test would have failed with // 'Expected: 430, Actual : -2542'. CPPUNIT_ASSERT_EQUAL(static_cast(430), nHoriOrientPosition); } DECLARE_OOXMLIMPORT_TEST(testTdf111550, "tdf111550.docx") { // The test document has following ill-formed structure: // // // ... // // // // // [outer:A2] // // // // // // // // [inner:A1] // // // // // // // // // // // i.e., a as direct child of inside another table. // Word accepts that illegal OOXML, and treats it as equal to // // // ... // // // // // // // // [outer:A2] // // // // [inner:A1] // // // // // // // // // // i.e., moves all contents of the outer paragraph into the inner table's first paragraph. CPPUNIT_ASSERT_EQUAL(2, getParagraphs()); uno::Reference outerTable = getParagraphOrTable(1); getCell(outerTable, "A1", "[outer:A1]"); uno::Reference cellA2(getCell(outerTable, "A2"), uno::UNO_QUERY_THROW); uno::Reference innerTable = getParagraphOrTable(1, cellA2); getCell(innerTable, "A1", "[outer:A2]\n[inner:A1]"); } DECLARE_OOXMLIMPORT_TEST(testTdf117843, "tdf117843.docx") { uno::Reference xPageStyles = getStyles("PageStyles"); uno::Reference xPageStyle(xPageStyles->getByName("Standard"), uno::UNO_QUERY); uno::Reference xHeaderText = getProperty>(xPageStyle, "HeaderText"); // This was 4025, increased top paragraph margin was unexpected. CPPUNIT_ASSERT_EQUAL( static_cast(0), getProperty(getParagraphOfText(1, xHeaderText), "ParaTopMargin")); } // related tdf#124754 DECLARE_OOXMLIMPORT_TEST(testTdf43017, "tdf43017.docx") { uno::Reference xParagraph = getParagraph(1); uno::Reference xText = getRun(xParagraph, 2, "kick the bucket"); // Ensure that hyperlink text color is not blue (0x0000ff), but default (-1) CPPUNIT_ASSERT_EQUAL_MESSAGE("Hyperlink color should be black!", sal_Int32(-1), getProperty(xText, "CharColor")); } CPPUNIT_TEST_FIXTURE(Test, testTdf127778) { load(mpTestDocumentPath, "tdf127778.docx"); xmlDocUniquePtr pLayout = parseLayoutDump(); // Without the accompanying fix in place, this test would have failed with: // equality assertion failed // - Expected: 0 // - Actual : 1 // i.e. the 2nd page had an unexpected header. assertXPath(pLayout, "//page[2]/header", 0); } // related tdf#43017 DECLARE_OOXMLIMPORT_TEST(testTdf124754, "tdf124754.docx") { uno::Reference textbox(getShape(1), uno::UNO_QUERY); CPPUNIT_ASSERT_EQUAL(1, getParagraphs(textbox)); uno::Reference xParagraph = getParagraphOfText(1, textbox); uno::Reference xText = getRun(xParagraph, 2); // Ensure that hyperlink text color is not black CPPUNIT_ASSERT_EQUAL_MESSAGE("Hyperlink color should be not black!", sal_Int32(353217), getProperty(xText, "CharColor")); } DECLARE_OOXMLIMPORT_TEST(testTextCopy, "text-copy.docx") { // The document has a header on the second page that is copied as part of the import process. // The header has a single paragraph: make sure shapes anchored to it are not lost. // Note that the single paragraph itself has no text portions. uno::Reference xTextDocument(mxComponent, uno::UNO_QUERY); uno::Reference xParaEnumAccess(xTextDocument->getText(), uno::UNO_QUERY); uno::Reference xParaEnum = xParaEnumAccess->createEnumeration(); uno::Reference xPara; while (xParaEnum->hasMoreElements()) { xPara.set(xParaEnum->nextElement(), uno::UNO_QUERY); } auto aPageStyleName = getProperty(xPara, "PageStyleName"); uno::Reference xPageStyle( getStyles("PageStyles")->getByName(aPageStyleName), uno::UNO_QUERY); auto xHeaderText = getProperty>(xPageStyle, "HeaderText"); uno::Reference xHeaderPara( getParagraphOfText(1, xHeaderText), uno::UNO_QUERY); uno::Reference xHeaderShapes = xHeaderPara->createContentEnumeration("com.sun.star.text.TextContent"); // Without the accompanying fix in place, this test would have failed with: // assertion failed // - Expression: xHeaderShapes->hasMoreElements() // i.e. the second page's header had no anchored shapes. CPPUNIT_ASSERT(xHeaderShapes->hasMoreElements()); } DECLARE_OOXMLIMPORT_TEST(testTdf112443, "tdf112443.docx") { // the position of the flying text frame should be off page // 30624 below its anchor OUString aTop = parseDump("//fly[1]/infos/bounds", "top"); CPPUNIT_ASSERT_EQUAL(OUString("30624"), aTop); } // DOCX: Textbox wrap differs in MSO and LO // Both should layout text regardless of existing text box // and as result only one page should be generated. DECLARE_OOXMLIMPORT_TEST(testTdf113182, "tdf113182.docx") { CPPUNIT_ASSERT_EQUAL(1, getPages()); } DECLARE_OOXMLIMPORT_TEST(testBtlrFrameVml, "btlr-frame-vml.docx") { uno::Reference xTextFrame(getShape(1), uno::UNO_QUERY); CPPUNIT_ASSERT(xTextFrame.is()); auto nActual = getProperty(xTextFrame, "WritingMode"); // Without the accompanying fix in place, this test would have failed with 'Expected: 5; Actual: // 4', i.e. writing direction was inherited from page, instead of explicit btlr. CPPUNIT_ASSERT_EQUAL(text::WritingMode2::BT_LR, nActual); } DECLARE_OOXMLIMPORT_TEST(testTdf124398, "tdf124398.docx") { uno::Reference xGroup(getShape(1), uno::UNO_QUERY); CPPUNIT_ASSERT(xGroup.is()); // Without the accompanying fix in place, this test would have failed with 'Expected: 2; Actual: // 1', i.e. the chart children of the group shape was lost. CPPUNIT_ASSERT_EQUAL(static_cast(2), xGroup->getCount()); uno::Reference xShape(xGroup->getByIndex(1), uno::UNO_QUERY); CPPUNIT_ASSERT_EQUAL(OUString("com.sun.star.drawing.OLE2Shape"), xShape->getShapeType()); } DECLARE_OOXMLIMPORT_TEST(testTdf104167, "tdf104167.docx") { // Make sure that heading 1 paragraphs start on a new page. uno::Any xStyle = getStyles("ParagraphStyles")->getByName("Heading 1"); // Without the accompanying fix in place, this test would have failed with: // - Expected: 4 // - Actual : 0 // i.e. the was lost on import. CPPUNIT_ASSERT_EQUAL(style::BreakType_PAGE_BEFORE, getProperty(xStyle, "BreakType")); } DECLARE_OOXMLIMPORT_TEST(testTdf113946, "tdf113946.docx") { OUString aTop = parseDump("/root/page/body/txt/anchored/SwAnchoredDrawObject/bounds", "top"); // tdf#106792 Checked loading of tdf113946.docx. Before the change, the expected // value of this test was "1696". Opening the file shows a single short line anchored // at the doc start. Only diff is that in 'old' version it is slightly rotated, in 'new' // version line is strict horizontal. Checked against MSWord2013, there the line // is also not rotated -> the change is to the better, correct the expected result here. CPPUNIT_ASSERT_EQUAL(OUString("1695"), aTop); } DECLARE_OOXMLIMPORT_TEST(testTdf121804, "tdf121804.docx") { uno::Reference xGroup(getShape(1), uno::UNO_QUERY); uno::Reference xShape(xGroup->getByIndex(0), uno::UNO_QUERY); uno::Reference xFirstPara = getParagraphOfText(1, xShape->getText()); uno::Reference xFirstRun = getRun(xFirstPara, 1); CPPUNIT_ASSERT_EQUAL(static_cast(0), getProperty(xFirstRun, "CharEscapement")); // This failed with a NoSuchElementException, super/subscript property was // lost on import, so the whole paragraph was a single run. uno::Reference xSecondRun = getRun(xFirstPara, 2); CPPUNIT_ASSERT_EQUAL(static_cast(30), getProperty(xSecondRun, "CharEscapement")); uno::Reference xThirdRun = getRun(xFirstPara, 3); CPPUNIT_ASSERT_EQUAL(static_cast(-25), getProperty(xThirdRun, "CharEscapement")); } DECLARE_OOXMLIMPORT_TEST(testTdf114217, "tdf114217.docx") { // This was 1, multi-page table was imported as a floating one. CPPUNIT_ASSERT_EQUAL(0, getShapes()); } DECLARE_OOXMLIMPORT_TEST(testTdf119200, "tdf119200.docx") { auto xPara = getParagraph(1); // Check that we import MathType functional symbols as symbols, not functions with missing args CPPUNIT_ASSERT_EQUAL(OUString(u" size 12{ func \u2208 } {}"), getFormula(getRun(xPara, 1))); CPPUNIT_ASSERT_EQUAL(OUString(u" size 12{ func \u2209 } {}"), getFormula(getRun(xPara, 2))); CPPUNIT_ASSERT_EQUAL(OUString(u" size 12{ func \u2282 } {}"), getFormula(getRun(xPara, 3))); CPPUNIT_ASSERT_EQUAL(OUString(u" size 12{ func \u2283 } {}"), getFormula(getRun(xPara, 4))); CPPUNIT_ASSERT_EQUAL(OUString(u" size 12{ func \u2284 } {}"), getFormula(getRun(xPara, 5))); CPPUNIT_ASSERT_EQUAL(OUString(u" size 12{ func \u2286 } {}"), getFormula(getRun(xPara, 6))); CPPUNIT_ASSERT_EQUAL(OUString(u" size 12{ func \u2287 } {}"), getFormula(getRun(xPara, 7))); } DECLARE_OOXMLIMPORT_TEST(testTdf115094, "tdf115094.docx") { // anchor of graphic has to be the text in the text frame // xray ThisComponent.DrawPage(1).Anchor.Text uno::Reference xShape(getShape(2), uno::UNO_QUERY); uno::Reference xText1 = xShape->getAnchor()->getText(); // xray ThisComponent.TextTables(0).getCellByName("A1") uno::Reference xTablesSupplier(mxComponent, uno::UNO_QUERY); uno::Reference xTables(xTablesSupplier->getTextTables(), uno::UNO_QUERY); uno::Reference xTable(xTables->getByIndex(0), uno::UNO_QUERY); uno::Reference xText2(xTable->getCellByName("A1"), uno::UNO_QUERY); CPPUNIT_ASSERT_EQUAL(xText1.get(), xText2.get()); } DECLARE_OOXMLIMPORT_TEST(testTdf115094v2, "tdf115094v2.docx") { // layoutInCell="1" combined with CPPUNIT_ASSERT(getProperty(getShapeByName("Grafik 18"), "IsFollowingTextFlow")); CPPUNIT_ASSERT(getProperty(getShapeByName("Grafik 19"), "IsFollowingTextFlow")); } DECLARE_OOXMLIMPORT_TEST(testTdf122224, "tdf122224.docx") { uno::Reference xTextTablesSupplier(mxComponent, uno::UNO_QUERY); uno::Reference xTables(xTextTablesSupplier->getTextTables(), uno::UNO_QUERY); uno::Reference xTable(xTables->getByIndex(0), uno::UNO_QUERY); uno::Reference xCell(xTable->getCellByName("A2"), uno::UNO_QUERY_THROW); // This was "** Expression is faulty **", because of the unnecessary DOCX number format string CPPUNIT_ASSERT_EQUAL(OUString("2000"), xCell->getString()); } DECLARE_OOXMLIMPORT_TEST(testTdf121440, "tdf121440.docx") { // Insert some text in front of footnote SwXTextDocument* pTextDoc = dynamic_cast(mxComponent.get()); CPPUNIT_ASSERT(pTextDoc); SwWrtShell* pWrtShell = pTextDoc->GetDocShell()->GetWrtShell(); SwRootFrame* pLayout(pWrtShell->GetLayout()); CPPUNIT_ASSERT(!pLayout->IsHideRedlines()); pWrtShell->Insert("test"); // Ensure that inserted text is not superscripted CPPUNIT_ASSERT_EQUAL_MESSAGE( "Inserted text should be not a superscript!", static_cast(0), getProperty(getRun(getParagraph(1), 1), "CharEscapement")); } DECLARE_OOXMLIMPORT_TEST(testTdf124670, "tdf124670.docx") { CPPUNIT_ASSERT_EQUAL(1, getParagraphs()); // We need to take xml:space attribute into account, even in w:document element uno::Reference paragraph = getParagraph(1); CPPUNIT_ASSERT_EQUAL( OUString("You won't believe, but that's how it was in markup of original bugdoc!"), paragraph->getString()); } DECLARE_OOXMLIMPORT_TEST(testTdf126114, "tdf126114.docx") { // The problem was that after the drop-down form field, also the placeholder string // was imported as text. Beside the duplication of the field, it also caused a crash. CPPUNIT_ASSERT_EQUAL(7, getLength()); } DECLARE_OOXMLIMPORT_TEST(testTdf127825, "tdf127825.docx") { // The document has a shape with Japanese-style text in it. The shape has relative size and also // has automatic height. SwXTextDocument* pTextDoc = dynamic_cast(mxComponent.get()); CPPUNIT_ASSERT(pTextDoc); SwWrtShell* pWrtShell = pTextDoc->GetDocShell()->GetWrtShell(); CPPUNIT_ASSERT(pWrtShell); SwRootFrame* pLayout = pWrtShell->GetLayout(); CPPUNIT_ASSERT(pLayout); SwFrame* pPage = pLayout->GetLower(); CPPUNIT_ASSERT(pPage); SwFrame* pBody = pPage->GetLower(); CPPUNIT_ASSERT(pBody); SwFrame* pText = pBody->GetLower(); CPPUNIT_ASSERT(pText); CPPUNIT_ASSERT(pText->GetDrawObjs()); const SwSortedObjs& rDrawObjs = *pText->GetDrawObjs(); CPPUNIT_ASSERT(rDrawObjs.size()); // Without the accompanying fix in place, this overlapped the footer area, not the body area. CPPUNIT_ASSERT(rDrawObjs[0]->GetObjRect().IsOver(pBody->getFrameArea())); } DECLARE_OOXMLIMPORT_TEST(testTdf103345, "numbering-circle.docx") { uno::Reference xPropertySet( getStyles("NumberingStyles")->getByName("WWNum1"), uno::UNO_QUERY); uno::Reference xLevels( xPropertySet->getPropertyValue("NumberingRules"), uno::UNO_QUERY); uno::Sequence aProps; xLevels->getByIndex(0) >>= aProps; // 1st level for (beans::PropertyValue const& prop : std::as_const(aProps)) { if (prop.Name == "NumberingType") { CPPUNIT_ASSERT_EQUAL(style::NumberingType::CIRCLE_NUMBER, prop.Value.get()); return; } } } DECLARE_OOXMLIMPORT_TEST(testTdf130214, "tdf130214.docx") { // Currently this file imports with errors because of tdf#126435; it must not segfault on load } DECLARE_OOXMLIMPORT_TEST(testTdf129659, "tdf129659.docx") { // don't crash on footnote with page break } DECLARE_OOXMLIMPORT_TEST(testTdf129912, "tdf129912.docx") { SwXTextDocument* pTextDoc = dynamic_cast(mxComponent.get()); CPPUNIT_ASSERT(pTextDoc); SwWrtShell* pWrtShell = pTextDoc->GetDocShell()->GetWrtShell(); CPPUNIT_ASSERT(pWrtShell); // Goto*FootnoteAnchor iterates the footnotes in a ring, so we need the amount of footnotes to stop the loop sal_Int32 nCount = pWrtShell->GetDoc()->GetFootnoteIdxs().size(); CPPUNIT_ASSERT_EQUAL(sal_Int32(5), nCount); // the expected footnote labels // TODO: the 5th label is actually wrong (missing the "PR" after the symbol part), but the "b" is there?! const sal_Unicode pLabel5[] = { u'\xF0D1', u'\xF031', u'\xF032', u'\x0062' }; const OUString sFootnoteLabels[] = { OUString(u'\xF0A7'), "1", "2", OUString(u'\xF020'), { pLabel5, SAL_N_ELEMENTS(pLabel5) } }; CPPUNIT_ASSERT_EQUAL(sal_Int32(SAL_N_ELEMENTS(sFootnoteLabels)), nCount); pWrtShell->GotoPrevFootnoteAnchor(); nCount--; while (nCount >= 0) { SwFormatFootnote aFootnoteNote; CPPUNIT_ASSERT(pWrtShell->GetCurFootnote(&aFootnoteNote)); OUString sNumStr = aFootnoteNote.GetNumStr(); if (sNumStr.isEmpty()) sNumStr = OUString::number(aFootnoteNote.GetNumber()); CPPUNIT_ASSERT_EQUAL(sFootnoteLabels[nCount], sNumStr); pWrtShell->GotoPrevFootnoteAnchor(); nCount--; } } // tests should only be added to ooxmlIMPORT *if* they fail round-tripping in ooxmlEXPORT CPPUNIT_PLUGIN_IMPLEMENT(); /* vim:set shiftwidth=4 softtabstop=4 expandtab: */