sw/inc/formatcontentcontrol.hxx | 4 - sw/qa/extras/ooxmlexport/data/tdf169101_datePicker.docx |binary sw/qa/extras/ooxmlexport/ooxmlexport25.cxx | 13 +++++ sw/source/core/crsr/datecontentcontrolbutton.cxx | 6 +- sw/source/core/txtnode/attrcontentcontrol.cxx | 41 ++++++++-------- sw/source/filter/ww8/docxattributeoutput.cxx | 15 ++++- sw/source/writerfilter/dmapper/DomainMapper_Impl.cxx | 3 - 7 files changed, 54 insertions(+), 28 deletions(-)
New commits: commit 9a16f47ccb3ee9ff509a56bba026568f29800120 Author: Justin Luth <[email protected]> AuthorDate: Tue Feb 10 20:38:37 2026 -0500 Commit: Justin Luth <[email protected]> CommitDate: Thu Feb 12 07:32:53 2026 +0100 tdf#169101 docx export: ensure valid ISO8601 Sdt fullDate If the fullDate value is not a valid ISO8601 format string then MS Word complains that the file is invalid. To fix this, I needed a mechanism similar to std::pair<bool, double> DateFieldmark::GetCurrentDate() that allows GetCurrentDate to know whether it is a valid date, and DateFieldmark::GetDateInStandardDateFormat to get the date in a standard format. In addition, I fixed a few other overlooked pieces. make CppunitTest_sw_ooxmlexport25 \ CPPUNIT_TEST_NAME=testTdf169101_datePicker This unit test forced me to create the fall-back mechanism: make CppunitTest_sw_ooxmlexport13 \ CPPUNIT_TEST_NAME=testInvalidDateFormField Change-Id: If6ea8022f82d70a10c9af9cede285855122ddf60 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/199118 Reviewed-by: Justin Luth <[email protected]> Tested-by: Jenkins diff --git a/sw/inc/formatcontentcontrol.hxx b/sw/inc/formatcontentcontrol.hxx index 91efffd11e28..a53d6b3b7329 100644 --- a/sw/inc/formatcontentcontrol.hxx +++ b/sw/inc/formatcontentcontrol.hxx @@ -292,10 +292,10 @@ public: void SetCurrentDateValue(double fCurrentDate); /// Parses m_aCurrentDate and returns it. - double GetCurrentDateValue() const; + std::optional<double> GetCurrentDateValue() const; /// Formats m_oSelectedDate, taking m_aDateFormat and m_aDateLanguage into account. - OUString GetDateString() const; + OUString GetDateString(bool bAsISO8601 = false) const; void SetPlainText(bool bPlainText) { m_bPlainText = bPlainText; } diff --git a/sw/qa/extras/ooxmlexport/data/tdf169101_datePicker.docx b/sw/qa/extras/ooxmlexport/data/tdf169101_datePicker.docx new file mode 100644 index 000000000000..0be76293c531 Binary files /dev/null and b/sw/qa/extras/ooxmlexport/data/tdf169101_datePicker.docx differ diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx b/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx index 0a4d2820d01e..c160cb326996 100644 --- a/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx +++ b/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx @@ -212,6 +212,19 @@ CPPUNIT_TEST_FIXTURE(Test, testTdf168988_grabbagDatePicker) u" 2012./2013."); } +CPPUNIT_TEST_FIXTURE(Test, testTdf169101_datePicker) +{ + createSwDoc("tdf169101_datePicker.docx"); + + save(TestFilter::DOCX); + xmlDocUniquePtr pXmlDoc = parseExport(u"word/document.xml"_ustr); + assertXPath(pXmlDoc, "//w:sdt", 1); // only one sdt + assertXPath(pXmlDoc, "//w:sdt/w:sdtPr/w:date", 1); // it is a date content control + // there is no valid date, so fullDate must not be provided (or MS Word says 'corrupt file') + assertXPathNoAttribute(pXmlDoc, "//w:sdt/w:sdtPr/w:date", "fullDate"); + assertXPathContent(pXmlDoc, "//w:sdt/w:sdtContent/w:r/w:t", u"Week #"); +} + CPPUNIT_TEST_FIXTURE(Test, testTdf170389_manyTabstops) { createSwDoc("tdf170389_manyTabstops.odt"); diff --git a/sw/source/core/crsr/datecontentcontrolbutton.cxx b/sw/source/core/crsr/datecontentcontrolbutton.cxx index 06f5f92cd691..26bc21bc99a0 100644 --- a/sw/source/core/crsr/datecontentcontrolbutton.cxx +++ b/sw/source/core/crsr/datecontentcontrolbutton.cxx @@ -50,10 +50,10 @@ void SwDateContentControlButton::LaunchPopup() if (m_pContentControl) { const Date& rNullDate = m_pNumberFormatter->GetNullDate(); - double fCurrentDate = m_pContentControl->GetCurrentDateValue(); - if (fCurrentDate != 0) + std::optional<double> ofCurrentDate = m_pContentControl->GetCurrentDateValue(); + if (ofCurrentDate.has_value()) { - m_xCalendar->set_date(rNullDate + sal_Int32(fCurrentDate)); + m_xCalendar->set_date(rNullDate + sal_Int32(*ofCurrentDate)); } } diff --git a/sw/source/core/txtnode/attrcontentcontrol.cxx b/sw/source/core/txtnode/attrcontentcontrol.cxx index c99b25b8b4f8..4f9d6407f5f2 100644 --- a/sw/source/core/txtnode/attrcontentcontrol.cxx +++ b/sw/source/core/txtnode/attrcontentcontrol.cxx @@ -369,41 +369,41 @@ void SwContentControl::ClearListItems() GetTextAttr()->Invalidate(); } -OUString SwContentControl::GetDateString() const +OUString SwContentControl::GetDateString(bool bAsISO8601) const { SwDoc& rDoc = m_pTextNode->GetDoc(); SvNumberFormatter* pNumberFormatter = rDoc.GetNumberFormatter(); - sal_uInt32 nFormat = pNumberFormatter->GetEntryKey( - m_aDateFormat, LanguageTag(m_aDateLanguage).getLanguageType()); + OUString aFormat = bAsISO8601 ? "YYYY-MM-DDTHH:MM:SSZ" : m_aDateFormat; + sal_uInt32 nFormat + = pNumberFormatter->GetEntryKey(aFormat, LanguageTag(m_aDateLanguage).getLanguageType()); if (nFormat == NUMBERFORMAT_ENTRY_NOT_FOUND) { // If not found, then create it. sal_Int32 nCheckPos = 0; SvNumFormatType nType; - OUString aFormat = m_aDateFormat; pNumberFormatter->PutEntry(aFormat, nCheckPos, nType, nFormat, LanguageTag(m_aDateLanguage).getLanguageType()); } + if (nFormat == NUMBERFORMAT_ENTRY_NOT_FOUND) + return OUString(); - const Color* pColor = nullptr; - OUString aFormatted; - double fSelectedDate = 0; + std::optional<double> ofSelectedDate; if (m_oSelectedDate) { - fSelectedDate = *m_oSelectedDate; + ofSelectedDate = *m_oSelectedDate; } else { - fSelectedDate = GetCurrentDateValue(); + ofSelectedDate = GetCurrentDateValue(); } - if (nFormat == NUMBERFORMAT_ENTRY_NOT_FOUND) - { + if (!ofSelectedDate.has_value()) return OUString(); - } - pNumberFormatter->GetOutputString(fSelectedDate, nFormat, aFormatted, &pColor, false); + const Color* pColor = nullptr; + OUString aFormatted; + pNumberFormatter->GetOutputString(*ofSelectedDate, nFormat, aFormatted, &pColor, false); return aFormatted; } @@ -430,34 +430,37 @@ void SwContentControl::SetCurrentDateValue(double fCurrentDate) const Color* pColor = nullptr; pNumberFormatter->GetOutputString(fCurrentDate, nFormat, aFormatted, &pColor, false); m_aCurrentDate = aFormatted + "T00:00:00Z"; + m_aDateFormat = CURRENT_DATE_FORMAT; } -double SwContentControl::GetCurrentDateValue() const +std::optional<double> SwContentControl::GetCurrentDateValue() const { if (m_aCurrentDate.isEmpty()) { - return 0; + return std::nullopt; } SwDoc& rDoc = m_pTextNode->GetDoc(); SvNumberFormatter* pNumberFormatter = rDoc.GetNumberFormatter(); - sal_uInt32 nFormat = pNumberFormatter->GetEntryKey(CURRENT_DATE_FORMAT, LANGUAGE_ENGLISH_US); + OUString sFormat = m_aDateFormat.isEmpty() ? CURRENT_DATE_FORMAT : m_aDateFormat; + sal_uInt32 nFormat = pNumberFormatter->GetEntryKey(sFormat, LANGUAGE_ENGLISH_US); if (nFormat == NUMBERFORMAT_ENTRY_NOT_FOUND) { sal_Int32 nCheckPos = 0; SvNumFormatType nType; - OUString sFormat = CURRENT_DATE_FORMAT; pNumberFormatter->PutEntry(sFormat, nCheckPos, nType, nFormat, LANGUAGE_ENGLISH_US); } if (nFormat == NUMBERFORMAT_ENTRY_NOT_FOUND) { - return 0; + return std::nullopt; } double dCurrentDate = 0; OUString aCurrentDate = m_aCurrentDate.replaceAll("T00:00:00Z", ""); - (void)pNumberFormatter->IsNumberFormat(aCurrentDate, nFormat, dCurrentDate); + if (!pNumberFormatter->IsNumberFormat(aCurrentDate, nFormat, dCurrentDate)) + return std::nullopt; + return dCurrentDate; } diff --git a/sw/source/filter/ww8/docxattributeoutput.cxx b/sw/source/filter/ww8/docxattributeoutput.cxx index 2a5fda85a065..705f6cca86e5 100644 --- a/sw/source/filter/ww8/docxattributeoutput.cxx +++ b/sw/source/filter/ww8/docxattributeoutput.cxx @@ -2787,14 +2787,23 @@ void DocxAttributeOutput::WriteContentControlStart() if (m_pContentControl->GetDate()) { - OUString aCurrentDate = m_pContentControl->GetCurrentDate(); - if (aCurrentDate.isEmpty()) + // fullDate must be a valid date (YYYY-MM-DDTHH:MM:SSZ) or MS Word calls the file corrupt + OUString aFullDate = m_pContentControl->GetDateString(/*bAsISO8601=*/true); + if (aFullDate.isEmpty()) + { + // Round-trip fullDate (if valid) in case it just doesn't match the display format + const OUString sDisplayVal = m_pContentControl->GetCurrentDate(); + if (sDisplayVal.getLength() == 20 && sDisplayVal[10] == 'T' && sDisplayVal[19] == 'Z') + aFullDate = sDisplayVal; + } + + if (aFullDate.isEmpty()) { m_pSerializer->startElementNS(XML_w, XML_date); } else { - m_pSerializer->startElementNS(XML_w, XML_date, FSNS(XML_w, XML_fullDate), aCurrentDate); + m_pSerializer->startElementNS(XML_w, XML_date, FSNS(XML_w, XML_fullDate), aFullDate); } OUString aDateFormat = m_pContentControl->GetDateFormat().replaceAll("\"", "'"); if (!aDateFormat.isEmpty()) diff --git a/sw/source/writerfilter/dmapper/DomainMapper_Impl.cxx b/sw/source/writerfilter/dmapper/DomainMapper_Impl.cxx index ffc2703608fb..614d1145dd06 100644 --- a/sw/source/writerfilter/dmapper/DomainMapper_Impl.cxx +++ b/sw/source/writerfilter/dmapper/DomainMapper_Impl.cxx @@ -1271,7 +1271,8 @@ void DomainMapper_Impl::PopSdt() { OUString aDateString; xContentControl->getPropertyValue(u"DateString"_ustr) >>= aDateString; - xCursor->setString(aDateString); + if (!aDateString.isEmpty()) + xCursor->setString(aDateString); } m_pSdtHelper->clear();
