diff options
-rw-r--r-- | include/xmloff/xmlnumfe.hxx | 1 | ||||
-rw-r--r-- | include/xmloff/xmltoken.hxx | 1 | ||||
-rw-r--r-- | sc/qa/unit/data/ods/tdf152724-Blank-width-char.ods | bin | 0 -> 8782 bytes | |||
-rw-r--r-- | sc/qa/unit/subsequent_export_test4.cxx | 13 | ||||
-rw-r--r-- | schema/libreoffice/OpenDocument-v1.3+libreoffice-schema.rng | 28 | ||||
-rw-r--r-- | xmloff/source/core/xmltoken.cxx | 1 | ||||
-rw-r--r-- | xmloff/source/style/xmlnumfe.cxx | 107 | ||||
-rw-r--r-- | xmloff/source/style/xmlnumfi.cxx | 103 | ||||
-rw-r--r-- | xmloff/source/token/tokens.txt | 1 |
9 files changed, 222 insertions, 33 deletions
diff --git a/include/xmloff/xmlnumfe.hxx b/include/xmloff/xmlnumfe.hxx index 43e1814dc26b..7c76e5117b1f 100644 --- a/include/xmloff/xmlnumfe.hxx +++ b/include/xmloff/xmlnumfe.hxx @@ -51,6 +51,7 @@ private: OUString m_sPrefix; SvNumberFormatter* m_pFormatter; OUStringBuffer m_sTextContent; + OUStringBuffer m_sBlankWidthString; bool m_bHasText; std::unique_ptr<SvXMLNumUsedList_Impl> m_pUsedList; std::unique_ptr<LocaleDataWrapper> m_pLocaleData; diff --git a/include/xmloff/xmltoken.hxx b/include/xmloff/xmltoken.hxx index e8ffd3fedcd5..7771577a453d 100644 --- a/include/xmloff/xmltoken.hxx +++ b/include/xmloff/xmltoken.hxx @@ -3466,6 +3466,7 @@ namespace xmloff::token { XML_ZEROS_DENOMINATOR_DIGITS, XML_INTEGER_FRACTION_DELIMITER, XML_MAX_BLANK_INTEGER_DIGITS, + XML_BLANK_WIDTH_CHAR, // tdf#115319 XML_REFERENCE_LANGUAGE, diff --git a/sc/qa/unit/data/ods/tdf152724-Blank-width-char.ods b/sc/qa/unit/data/ods/tdf152724-Blank-width-char.ods Binary files differnew file mode 100644 index 000000000000..4716d4be5355 --- /dev/null +++ b/sc/qa/unit/data/ods/tdf152724-Blank-width-char.ods diff --git a/sc/qa/unit/subsequent_export_test4.cxx b/sc/qa/unit/subsequent_export_test4.cxx index 74313ef67c27..357ac567575f 100644 --- a/sc/qa/unit/subsequent_export_test4.cxx +++ b/sc/qa/unit/subsequent_export_test4.cxx @@ -1480,6 +1480,19 @@ CPPUNIT_TEST_FIXTURE(ScExportTest4, testSecondsWithoutTruncateAndDecimals) lcl_TestNumberFormat(*getScDoc(), "[SS].00"); } +CPPUNIT_TEST_FIXTURE(ScExportTest4, testBlankWidthCharacter) +{ + createScDoc("ods/tdf152724-Blank-width-char.ods"); + + // save to ODS and reload + saveAndReload("calc8"); + lcl_TestNumberFormat(*getScDoc(), "[>0]_-?0;[<0]-?0;_-?0;@"); + + // save to XLSX and reload + saveAndReload("Calc Office Open XML"); + lcl_TestNumberFormat(*getScDoc(), "_-?0;-?0;_-?0;@"); +} + CPPUNIT_TEST_FIXTURE(ScExportTest4, testEmbeddedTextInDecimal) { createScDoc("xlsx/embedded-text-in-decimal.xlsx"); diff --git a/schema/libreoffice/OpenDocument-v1.3+libreoffice-schema.rng b/schema/libreoffice/OpenDocument-v1.3+libreoffice-schema.rng index b1a344e7bf4a..b7ad4b8a1a7c 100644 --- a/schema/libreoffice/OpenDocument-v1.3+libreoffice-schema.rng +++ b/schema/libreoffice/OpenDocument-v1.3+libreoffice-schema.rng @@ -2047,6 +2047,14 @@ xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1. <rng:empty/> </rng:element> </rng:define> + <rng:define name="number-text"> + <rng:element name="number:text"> + <rng:optional> + <rng:ref name="number-text-attlist"/> + </rng:optional> + <rng:text/> + </rng:element> + </rng:define> </rng:include> @@ -2745,6 +2753,26 @@ xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1. </rng:optional> </rng:define> + <rng:define name="number-embedded-text-attlist" combine="interleave"> + <!-- TODO no proposal, --> + <rng:optional> + <rng:attribute name="loext:blank-width-char"> + <rng:ref name="string"/> + </rng:attribute> + </rng:optional> + </rng:define> + + <!-- TODO no proposal, --> + <rng:define name="number-text-attlist"> + <rng:interleave> + <rng:optional> + <rng:attribute name="loext:blank-width-char"> + <rng:ref name="string"/> + </rng:attribute> + </rng:optional> + </rng:interleave> + </rng:define> + <!-- TODO no proposal --> <rng:define name="table-data-pilot-level-attlist" combine="interleave"> <rng:optional> diff --git a/xmloff/source/core/xmltoken.cxx b/xmloff/source/core/xmltoken.cxx index 8127996aa1c9..4f6f2223e721 100644 --- a/xmloff/source/core/xmltoken.cxx +++ b/xmloff/source/core/xmltoken.cxx @@ -3471,6 +3471,7 @@ namespace xmloff::token { TOKEN( "zeros-denominator-digits", XML_ZEROS_DENOMINATOR_DIGITS ), TOKEN( "integer-fraction-delimiter", XML_INTEGER_FRACTION_DELIMITER ), TOKEN( "max-blank-integer-digits", XML_MAX_BLANK_INTEGER_DIGITS ), + TOKEN( "blank-width-char", XML_BLANK_WIDTH_CHAR ), // for optional language-dependent reference formats TOKEN( "reference-language", XML_REFERENCE_LANGUAGE ), diff --git a/xmloff/source/style/xmlnumfe.cxx b/xmloff/source/style/xmlnumfe.cxx index fb767dc3a10d..67675cf22ab3 100644 --- a/xmloff/source/style/xmlnumfe.cxx +++ b/xmloff/source/style/xmlnumfe.cxx @@ -66,9 +66,10 @@ struct SvXMLEmbeddedTextEntry sal_uInt16 nSourcePos; // position in NumberFormat (to skip later) sal_Int32 nFormatPos; // resulting position in embedded-text element OUString aText; + bool isBlankWidth; // "_x" - SvXMLEmbeddedTextEntry( sal_uInt16 nSP, sal_Int32 nFP, OUString aT ) : - nSourcePos(nSP), nFormatPos(nFP), aText(std::move(aT)) {} + SvXMLEmbeddedTextEntry( sal_uInt16 nSP, sal_Int32 nFP, OUString aT, bool bBW = false ) : + nSourcePos(nSP), nFormatPos(nFP), aText(std::move(aT)), isBlankWidth( bBW ) {} }; } @@ -323,6 +324,16 @@ void SvXMLNumFmtExport::FinishTextElement_Impl(bool bUseExtensionNS) { if ( m_bHasText ) { + if ( !m_sBlankWidthString.isEmpty() ) + { + // Export only for 1.3 with extensions and later. + SvtSaveOptions::ODFSaneDefaultVersion eVersion = m_rExport.getSaneDefaultVersion(); + if (eVersion > SvtSaveOptions::ODFSVER_013 && ( (eVersion & SvtSaveOptions::ODFSVER_EXTENDED) != 0 )) + { + m_rExport.AddAttribute( XML_NAMESPACE_LO_EXT, XML_BLANK_WIDTH_CHAR, + m_sBlankWidthString.makeStringAndClear() ); + } + } sal_uInt16 nNS = bUseExtensionNS ? XML_NAMESPACE_LO_EXT : XML_NAMESPACE_NUMBER; SvXMLElementExport aElem( m_rExport, nNS, XML_TEXT, true, false ); @@ -502,6 +513,36 @@ void SvXMLNumFmtExport::WriteRepeatedElement_Impl( sal_Unicode nChar ) } } +namespace { +void lcl_WriteBlankWidthString( std::u16string_view rBlankWidthChar, OUStringBuffer& rBlankWidthString, OUStringBuffer& rTextContent ) +{ + // export "_x" + if ( rBlankWidthString.isEmpty() ) + { + rBlankWidthString.append( rBlankWidthChar ); + if ( !rTextContent.isEmpty() ) + { + // add position in rTextContent + rBlankWidthString.append( rTextContent.getLength() ); + } + } + else + { + // add "_" as separator if there are several blank width char + rBlankWidthString.append( "_" ); + rBlankWidthString.append( rBlankWidthChar ); + rBlankWidthString.append( rTextContent.getLength() ); + } + // for previous versions, turn "_x" into the number of spaces used for x in InsertBlanks in the NumberFormat + if ( !rBlankWidthChar.empty() ) + { + OUString aBlanks; + SvNumberformat::InsertBlanks( aBlanks, 0, rBlankWidthChar[0] ); + rTextContent.append( aBlanks ); + } +} +} + void SvXMLNumFmtExport::WriteSecondsElement_Impl( bool bLong, sal_uInt16 nDecimals ) { FinishTextElement_Impl(); @@ -553,28 +594,45 @@ void SvXMLNumFmtExport::WriteIntegerElement_Impl( void SvXMLNumFmtExport::WriteEmbeddedEntries_Impl( const SvXMLEmbeddedTextEntryArr& rEmbeddedEntries ) { auto nEntryCount = rEmbeddedEntries.size(); + SvtSaveOptions::ODFSaneDefaultVersion eVersion = m_rExport.getSaneDefaultVersion(); for (decltype(nEntryCount) nEntry=0; nEntry < nEntryCount; ++nEntry) { - const SvXMLEmbeddedTextEntry *const pObj = &rEmbeddedEntries[nEntry]; + const SvXMLEmbeddedTextEntry* pObj = &rEmbeddedEntries[nEntry]; // position attribute // position == 0 is between first integer digit and decimal separator // position < 0 is inside decimal part m_rExport.AddAttribute( XML_NAMESPACE_NUMBER, XML_POSITION, OUString::number( pObj->nFormatPos ) ); - SvXMLElementExport aChildElem( m_rExport, XML_NAMESPACE_NUMBER, XML_EMBEDDED_TEXT, - true, false ); // text as element content - OUStringBuffer aContent( pObj->aText ); - while ( nEntry+1 < nEntryCount && rEmbeddedEntries[nEntry+1].nFormatPos == pObj->nFormatPos ) + OUStringBuffer aContent; + OUStringBuffer aBlankWidthString; + do { - // The array can contain several elements for the same position in the number - // (for example, literal text and space from underscores). They must be merged - // into a single embedded-text element. - aContent.append(rEmbeddedEntries[nEntry+1].aText); + pObj = &rEmbeddedEntries[nEntry]; + if ( pObj->isBlankWidth ) + { + // (#i20396# the spaces may also be in embedded-text elements) + lcl_WriteBlankWidthString( pObj->aText, aBlankWidthString, aContent ); + } + else + { + // The array can contain several elements for the same position in the number. + // Literal texts are merged into a single embedded-text element. + aContent.append( pObj->aText ); + } ++nEntry; } + while ( nEntry < nEntryCount + && rEmbeddedEntries[nEntry].nFormatPos == pObj->nFormatPos ); + --nEntry; + + // Export only for 1.3 with extensions and later. + if ( !aBlankWidthString.isEmpty() && eVersion > SvtSaveOptions::ODFSVER_013 && ( (eVersion & SvtSaveOptions::ODFSVER_EXTENDED) != 0 ) ) + m_rExport.AddAttribute( XML_NAMESPACE_LO_EXT, XML_BLANK_WIDTH_CHAR, aBlankWidthString.makeStringAndClear() ); + SvXMLElementExport aChildElem( m_rExport, XML_NAMESPACE_NUMBER, XML_EMBEDDED_TEXT, + true, false ); m_rExport.Characters( aContent.makeStringAndClear() ); } } @@ -1196,13 +1254,13 @@ void SvXMLNumFmtExport::ExportPart_Impl( const SvNumberformat& rFormat, sal_uInt aAttr.Style ); } + SvtSaveOptions::ODFSaneDefaultVersion eVersion = m_rExport.getSaneDefaultVersion(); if ( !aAttr.Spellout.isEmpty() ) { const bool bWriteSpellout = aAttr.Format.isEmpty(); assert(bWriteSpellout); // mutually exclusive // Export only for 1.2 and later with extensions - SvtSaveOptions::ODFSaneDefaultVersion eVersion = m_rExport.getSaneDefaultVersion(); // Also ensure that duplicated transliteration-language and // transliteration-country attributes never escape into the wild with // releases. @@ -1389,7 +1447,6 @@ void SvXMLNumFmtExport::ExportPart_Impl( const SvNumberformat& rFormat, sal_uInt } // collect strings for embedded-text (must be known before number element is written) - SvtSaveOptions::ODFSaneDefaultVersion eVersion = m_rExport.getSaneDefaultVersion(); bool bAllowEmbedded = ( nFmtType == SvNumFormatType::ALL || nFmtType == SvNumFormatType::NUMBER || nFmtType == SvNumFormatType::CURRENCY || // Export only for 1.x with extensions @@ -1429,18 +1486,25 @@ void SvXMLNumFmtExport::ExportPart_Impl( const SvNumberformat& rFormat, sal_uInt // text (literal or underscore) within the integer (>=0) or decimal (<0) part of a number:number element OUString aEmbeddedStr; + bool bSaveBlankWidthSymbol = false; if ( nElemType == NF_SYMBOLTYPE_STRING || nElemType == NF_SYMBOLTYPE_PERCENT ) { aEmbeddedStr = *pElemStr; } else if (pElemStr->getLength() >= 2) { - SvNumberformat::InsertBlanks( aEmbeddedStr, 0, (*pElemStr)[1] ); + if ( eVersion > SvtSaveOptions::ODFSVER_013 && ( (eVersion & SvtSaveOptions::ODFSVER_EXTENDED) != 0 ) ) + { + aEmbeddedStr = pElemStr->copy( 1, 1 ); + bSaveBlankWidthSymbol = true; + } + else // turn "_x" into the number of spaces used for x in InsertBlanks in the NumberFormat + SvNumberformat::InsertBlanks( aEmbeddedStr, 0, (*pElemStr)[1] ); } sal_Int32 nEmbedPos = nIntegerSymbols - nDigitsPassed; aEmbeddedEntries.push_back( - SvXMLEmbeddedTextEntry(nPos, nEmbedPos, aEmbeddedStr)); + SvXMLEmbeddedTextEntry( nPos, nEmbedPos, aEmbeddedStr, bSaveBlankWidthSymbol )); } break; } @@ -1505,13 +1569,12 @@ void SvXMLNumFmtExport::ExportPart_Impl( const SvNumberformat& rFormat, sal_uInt case NF_SYMBOLTYPE_BLANK: if ( pElemStr && !lcl_IsInEmbedded( aEmbeddedEntries, nPos ) ) { - // turn "_x" into the number of spaces used for x in InsertBlanks in the NumberFormat - // (#i20396# the spaces may also be in embedded-text elements) - - OUString aBlanks; - if (pElemStr->getLength() >= 2) - SvNumberformat::InsertBlanks( aBlanks, 0, (*pElemStr)[1] ); - AddToTextElement_Impl( aBlanks ); + if ( pElemStr->getLength() == 2 ) + { + OUString aBlankWidthChar = pElemStr->copy( 1 ); + lcl_WriteBlankWidthString( aBlankWidthChar, m_sBlankWidthString, m_sTextContent ); + m_bHasText = true; + } } break; case NF_KEY_GENERAL : diff --git a/xmloff/source/style/xmlnumfi.cxx b/xmloff/source/style/xmlnumfi.cxx index e43e732a4ba8..f9f9bce5f675 100644 --- a/xmloff/source/style/xmlnumfi.cxx +++ b/xmloff/source/style/xmlnumfi.cxx @@ -123,6 +123,7 @@ class SvXMLNumFmtElementContext : public SvXMLImportContext bool bLong; bool bTextual; OUString sCalendar; + OUString sBlankWidthString; public: SvXMLNumFmtElementContext( SvXMLImport& rImport, sal_Int32 nElement, @@ -134,7 +135,7 @@ public: virtual void SAL_CALL characters( const OUString& rChars ) override; virtual void SAL_CALL endFastElement(sal_Int32 nElement) override; - void AddEmbeddedElement( sal_Int32 nFormatPos, const OUString& rContent ); + void AddEmbeddedElement( sal_Int32 nFormatPos, std::u16string_view rContent, std::u16string_view rBlankWidthString ); }; class SvXMLNumFmtEmbeddedTextContext : public SvXMLImportContext @@ -142,6 +143,7 @@ class SvXMLNumFmtEmbeddedTextContext : public SvXMLImportContext SvXMLNumFmtElementContext& rParent; OUStringBuffer aContent; sal_Int32 nTextPosition; + OUString aBlankWidthString; public: SvXMLNumFmtEmbeddedTextContext( SvXMLImport& rImport, sal_Int32 nElement, @@ -461,6 +463,11 @@ SvXMLNumFmtEmbeddedTextContext::SvXMLNumFmtEmbeddedTextContext( SvXMLImport& rIm if (::sax::Converter::convertNumber( nAttrVal, aIter.toView() )) nTextPosition = nAttrVal; } + else if ( aIter.getToken() == XML_ELEMENT(LO_EXT, XML_BLANK_WIDTH_CHAR) + || aIter.getToken() == XML_ELEMENT(NUMBER, XML_BLANK_WIDTH_CHAR) ) + { + aBlankWidthString = aIter.toString(); + } else XMLOFF_WARN_UNKNOWN("xmloff", aIter); } @@ -473,7 +480,7 @@ void SvXMLNumFmtEmbeddedTextContext::characters( const OUString& rChars ) void SvXMLNumFmtEmbeddedTextContext::endFastElement(sal_Int32 ) { - rParent.AddEmbeddedElement( nTextPosition, aContent.makeStringAndClear() ); + rParent.AddEmbeddedElement( nTextPosition, aContent.makeStringAndClear(), aBlankWidthString ); } static bool lcl_ValidChar( sal_Unicode cChar, const SvXMLNumFormatContext& rParent ) @@ -778,6 +785,10 @@ SvXMLNumFmtElementContext::SvXMLNumFmtElementContext( SvXMLImport& rImport, case XML_ELEMENT(NUMBER, XML_CALENDAR): sCalendar = aIter.toString(); break; + case XML_ELEMENT(NUMBER, XML_BLANK_WIDTH_CHAR): + case XML_ELEMENT(LO_EXT, XML_BLANK_WIDTH_CHAR): + sBlankWidthString = aIter.toString(); + break; default: XMLOFF_WARN_UNKNOWN("xmloff", aIter); } @@ -856,15 +867,83 @@ void SvXMLNumFmtElementContext::characters( const OUString& rChars ) aContent.append( rChars ); } -void SvXMLNumFmtElementContext::AddEmbeddedElement( sal_Int32 nFormatPos, const OUString& rContent ) +namespace { +void lcl_InsertBlankWidthChars( std::u16string_view rBlankWidthString, OUStringBuffer& rContent ) { - if (rContent.isEmpty()) - return; + sal_Int32 nShiftPosition = 1; // rContent starts with a quote + const size_t nLenBlank = rBlankWidthString.size(); + for ( size_t i = 0 ; i < nLenBlank ; i++ ) + { + sal_Unicode nChar = rBlankWidthString[ i ]; + OUString aBlanks; + SvNumberformat::InsertBlanks( aBlanks, 0, nChar ); + sal_Int32 nPositionContent = 0; + if ( ++i < nLenBlank ) + { + sal_Int32 nNext = rBlankWidthString.find( '_', i ); + if ( static_cast<sal_Int32>( i ) < nNext ) + { + nPositionContent = o3tl::toInt32( rBlankWidthString.substr( i, nNext - i ) ); + i = nNext; + } + else + nPositionContent = o3tl::toInt32( rBlankWidthString.substr( i ) ); + } + nPositionContent += nShiftPosition; + if ( nPositionContent >= 0 ) + { + rContent.remove( nPositionContent, aBlanks.getLength() ); + if ( nPositionContent >= 1 && rContent[ nPositionContent-1 ] == '\"' ) + { + nPositionContent--; + rContent.insert( nPositionContent, nChar ); + rContent.insert( nPositionContent, '_' ); + } + else + { + rContent.insert( nPositionContent, '\"' ); + rContent.insert( nPositionContent, nChar ); + rContent.insert( nPositionContent, "\"_" ); + nShiftPosition += 2; + } + // rContent length was modified: remove blanks, add "_x" + nShiftPosition += 2 - aBlanks.getLength(); + } + } + // remove empty string at the end of rContent + if ( std::u16string_view( rContent ).substr( rContent.getLength() - 2 ) == u"\"\"" ) + { + sal_Int32 nLen = rContent.getLength(); + if ( nLen >= 3 && rContent[ nLen-3 ] != '\\' ) + rContent.truncate( nLen - 2 ); + } +} +} - auto iterPair = aNumInfo.m_EmbeddedElements.emplace(nFormatPos, rContent); +void SvXMLNumFmtElementContext::AddEmbeddedElement( sal_Int32 nFormatPos, std::u16string_view rContentEmbedded, std::u16string_view rBlankWidthString ) +{ + if ( rContentEmbedded.empty() ) + return; + OUStringBuffer aContentEmbedded( rContentEmbedded ); + // #107805# always quote embedded strings - even space would otherwise + // be recognized as thousands separator in French. + aContentEmbedded.insert( 0, '"' ); + aContentEmbedded.append( '"' ); + if ( !rBlankWidthString.empty() ) + lcl_InsertBlankWidthChars( rBlankWidthString, aContentEmbedded ); + + auto iterPair = aNumInfo.m_EmbeddedElements.emplace( nFormatPos, aContentEmbedded.toString() ); if (!iterPair.second) + { // there's already an element at this position - append text to existing element - iterPair.first->second += rContent; + if ( iterPair.first->second.endsWith( "\"" ) && aContentEmbedded[ 0 ] == '"' ) + { // remove double quote + iterPair.first->second = OUString::Concat( iterPair.first->second.subView( 0, iterPair.first->second.getLength() - 1 ) ) + + aContentEmbedded.subView( 1, aContentEmbedded.getLength() - 1 ); + } + else + iterPair.first->second += aContentEmbedded; + } } void SvXMLNumFmtElementContext::endFastElement(sal_Int32 ) @@ -889,6 +968,11 @@ void SvXMLNumFmtElementContext::endFastElement(sal_Int32 ) if ( !aContent.isEmpty() ) { lcl_EnquoteIfNecessary( aContent, rParent ); + if ( !sBlankWidthString.isEmpty() ) + { + lcl_InsertBlankWidthChars( sBlankWidthString, aContent ); + sBlankWidthString = ""; + } rParent.AddToCode( aContent ); aContent.setLength(0); } @@ -1818,10 +1902,7 @@ void SvXMLNumFormatContext::AddNumber( const SvXMLNumberInfo& rInfo ) sal_Int32 nInsertPos = nZeroPos - nFormatPos; if ( nInsertPos >= 0 ) { - // #107805# always quote embedded strings - even space would otherwise - // be recognized as thousands separator in French. - - aNumStr.insert(nInsertPos, OUString::Concat("\"") + it.second + "\""); + aNumStr.insert( nInsertPos, it.second ); } } } diff --git a/xmloff/source/token/tokens.txt b/xmloff/source/token/tokens.txt index e8022d14dfce..469d03645276 100644 --- a/xmloff/source/token/tokens.txt +++ b/xmloff/source/token/tokens.txt @@ -3227,6 +3227,7 @@ zeros-numerator-digits zeros-denominator-digits integer-fraction-delimiter max-blank-integer-digits +blank-width-char reference-language newline creator-initials |