From 1ac50fddc77c0501fbedea80b527d577414f1bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Vajngerl?= Date: Thu, 20 Aug 2026 16:08:16 +0900 Subject: [PATCH] sc: rewrite the token array into the OOXML shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OOXML writes @ and # as calls and a reference union as a parenthesised list, so for each of them we need to know how far the sub-expression under it reaches. CreateStringFromTokenArray worked that out from the text it had already written. The problem with this is that every pair it spliced in shifted all the offsets that were still waiting behind it, so it kept breaking. Instead of patching the text, OOXMLRewriter builds a second token array in the shape OOXML spells out and the writer walks that as it stands. The call names and the parentheses are ordinary tokens, so there is nothing to splice and no offsets to keep track of. It is the same trick RewriteMissing already plays a few lines further up, and it means the input and the output can be looked at side by side. The # changes place, from behind its operand to a call in front of it, so it gets an opcode of its own. _xlfn.ANCHORARRAY spells ocAnchorArray now, and ocSpill keeps the native postfix spelling with no OOXML one at all, so the postfix form cannot end up in a file by accident. The import already rewrote the call back into the postfix operator and now recognises it by opcode instead of by position. The @ stays ocSingleValue, it is already in front of its operand so only the parentheses are new. The OFFSET() literal argument is swapped for the reference error in the same pass, instead of the writing loop jumping over tokens. A unit test drives the rewrite on its own: token array in, token array out, one case per shape. The shapes that end up with nothing to wrap are in there too - a lone @, one with a missing argument, and text that never parsed - since those are the ones where dropping a token is the only change. Also drop the dead "@" and "#" rows from the OOXML symbol table, the rows above them already have _xlfn.SINGLE and _xlfn.ANCHORARRAY. Signed-off-by: Tomaž Vajngerl Change-Id: I63944fb85b5c4388f304f7d94afd0b4f347d7782 Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/10155 Tested-by: Tomaž Vajngerl Reviewed-by: Miklos Vajna (cherry picked from commit 4feac84950f5b524d99e277481de50f7b45a8d2f) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/210292 Reviewed-by: Xisco Fauli Tested-by: Jenkins --- formula/CppunitTest_formula_ooxml.mk | 43 ++ formula/Library_for.mk | 1 + formula/Module_formula.mk | 4 + formula/inc/OOXMLRewriter.hxx | 45 ++ formula/inc/core_resource.hrc | 4 +- formula/qa/unit/OOXMLRewriterTest.cxx | 306 +++++++++++ formula/source/core/api/FormulaCompiler.cxx | 431 +--------------- formula/source/core/api/OOXMLRewriter.cxx | 534 ++++++++++++++++++++ include/formula/opcode.hxx | 5 +- sc/qa/unit/subsequent_export_test4.cxx | 25 + sc/qa/unit/ucalc_formula2.cxx | 43 +- sc/source/filter/oox/formulabuffer.cxx | 16 +- 12 files changed, 1021 insertions(+), 436 deletions(-) create mode 100644 formula/CppunitTest_formula_ooxml.mk create mode 100644 formula/inc/OOXMLRewriter.hxx create mode 100644 formula/qa/unit/OOXMLRewriterTest.cxx create mode 100644 formula/source/core/api/OOXMLRewriter.cxx diff --git a/formula/CppunitTest_formula_ooxml.mk b/formula/CppunitTest_formula_ooxml.mk new file mode 100644 index 000000000000..5c082b9a47e5 --- /dev/null +++ b/formula/CppunitTest_formula_ooxml.mk @@ -0,0 +1,43 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t; fill-column: 100 -*- +# +# Copyright the LibreOffice contributors. +# +# SPDX-License-Identifier: MPL-2.0 +# +# 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/. +# + +$(eval $(call gb_CppunitTest_CppunitTest,formula_ooxml)) + +$(eval $(call gb_CppunitTest_use_sdk_api,formula_ooxml)) + +$(eval $(call gb_CppunitTest_use_ure,formula_ooxml)) + +$(eval $(call gb_CppunitTest_set_include,formula_ooxml,\ + -I$(SRCDIR)/formula/inc \ + $$(INCLUDE) \ +)) + +$(eval $(call gb_CppunitTest_add_exception_objects,formula_ooxml,\ + formula/qa/unit/OOXMLRewriterTest \ +)) + +$(eval $(call gb_CppunitTest_use_libraries,formula_ooxml,\ + comphelper \ + cppu \ + cppuhelper \ + for \ + i18nlangtag \ + sal \ + svl \ + svt \ + tl \ + utl \ + vcl \ +)) + +$(eval $(call gb_CppunitTest_use_external,formula_ooxml,boost_headers)) + +# vim: set noet sw=4 ts=4: diff --git a/formula/Library_for.mk b/formula/Library_for.mk index e1fc7d776866..5de3597f23a2 100644 --- a/formula/Library_for.mk +++ b/formula/Library_for.mk @@ -42,6 +42,7 @@ $(eval $(call gb_Library_add_exception_objects,for,\ formula/source/core/api/FormulaCompiler \ formula/source/core/api/FormulaOpCodeMapperObj \ formula/source/core/api/grammar \ + formula/source/core/api/OOXMLRewriter \ formula/source/core/api/token \ formula/source/core/api/vectortoken \ formula/source/core/resource/core_resource \ diff --git a/formula/Module_formula.mk b/formula/Module_formula.mk index eac8702bf5ff..ad27bd677bc3 100644 --- a/formula/Module_formula.mk +++ b/formula/Module_formula.mk @@ -15,6 +15,10 @@ $(eval $(call gb_Module_add_targets,formula,\ UIConfig_formula \ )) +$(eval $(call gb_Module_add_check_targets,formula,\ + CppunitTest_formula_ooxml \ +)) + $(eval $(call gb_Module_add_l10n_targets,formula,\ AllLangMoTarget_for \ )) diff --git a/formula/inc/OOXMLRewriter.hxx b/formula/inc/OOXMLRewriter.hxx new file mode 100644 index 000000000000..bd82e97f7d8b --- /dev/null +++ b/formula/inc/OOXMLRewriter.hxx @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * Copyright the LibreOffice contributors. + * + * SPDX-License-Identifier: MPL-2.0 + * + * 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/. + */ + +#pragma once + +#include + +#include + +namespace formula +{ +class FormulaTokenArray; + +/** Rewrites the token array to make it compatible with OOXML. + + OOXML writes @ and # as calls and a reference union as a parenthesised list. Those call + names and parentheses become tokens here, so writing the result is a plain walk. The # + also moves in front of its operand and becomes ocAnchorArray. + + Operator extents come from the token order, so this restates the precedence the parser + applied. Change one and the other needs it too. + */ +class FORMULA_DLLPUBLIC OOXMLRewriter +{ +public: + explicit OOXMLRewriter(FormulaTokenArray const& rTokens); + + /** The OOXML compatible token array, or nullptr. The caller takes ownership. */ + std::unique_ptr releaseTokens() { return std::move(mpRewritten); } + +private: + std::unique_ptr mpRewritten; +}; + +} // namespace formula + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/formula/inc/core_resource.hrc b/formula/inc/core_resource.hrc index 3bfb26783010..43fea5c6fc04 100644 --- a/formula/inc/core_resource.hrc +++ b/formula/inc/core_resource.hrc @@ -516,7 +516,7 @@ const std::pair RID_STRLIST_FUNCTION_NAMES_ENGLISH_OOXML[] = { "IFERROR" , ocIfError }, { "_xlfn.IFNA" , ocIfNA }, { "_xlfn.SINGLE" , ocSingleValue }, - { "_xlfn.ANCHORARRAY" , ocSpill }, + { "_xlfn.ANCHORARRAY" , ocAnchorArray }, { "CHOOSE" , ocChoose }, { "(" , ocOpen }, { ")" , ocClose }, @@ -554,8 +554,6 @@ const std::pair RID_STRLIST_FUNCTION_NAMES_ENGLISH_OOXML[] = { "NOT" , ocNot }, { "NEG" , ocNeg }, { "-" , ocNegSub }, - { "@" , ocSingleValue }, - { "#" , ocSpill }, { "PI" , ocPi }, { "RAND" , ocRandom }, { "TRUE" , ocTrue }, diff --git a/formula/qa/unit/OOXMLRewriterTest.cxx b/formula/qa/unit/OOXMLRewriterTest.cxx new file mode 100644 index 000000000000..b6c41a5cf766 --- /dev/null +++ b/formula/qa/unit/OOXMLRewriterTest.cxx @@ -0,0 +1,306 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * Copyright the LibreOffice contributors. + * + * SPDX-License-Identifier: MPL-2.0 + * + * 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/. + */ + +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +using namespace formula; + +namespace +{ +class OOXMLRewriterTest : public CppUnit::TestFixture +{ +protected: + /** Add an operand. The rewrite reads opcodes and stack types, never whether a token is a + reference, so any push token does. A number is the shortest to build. */ + static void addOperand(FormulaTokenArray& rTokens) { rTokens.AddDouble(1.0); } + + static std::unique_ptr rewrite(const FormulaTokenArray& rTokens) + { + return OOXMLRewriter(rTokens).releaseTokens(); + } + + /** The token sequence as a readable line, so a failure says what came out. The names are + the tokens' own, not the text: a union stays ~ here, though OOXML writes it as a + comma. */ + static OUString describe(const FormulaTokenArray& rTokens) + { + OUStringBuffer aBuffer; + for (sal_uInt16 nPosition = 0; nPosition < rTokens.GetLen(); ++nPosition) + { + if (!aBuffer.isEmpty()) + aBuffer.append(' '); + aBuffer.append(name(rTokens.TokenAt(nPosition)->GetOpCode())); + } + return aBuffer.makeStringAndClear(); + } + +private: + static OUString name(OpCode eOp) + { + switch (eOp) + { + case ocPush: + return u"operand"_ustr; + case ocOpen: + return u"("_ustr; + case ocClose: + return u")"_ustr; + case ocUnion: + return u"~"_ustr; + case ocAdd: + return u"+"_ustr; + case ocSingleValue: + return u"_xlfn.SINGLE"_ustr; + case ocSpill: + return u"#"_ustr; + case ocAnchorArray: + return u"_xlfn.ANCHORARRAY"_ustr; + case ocOffset: + return u"OFFSET"_ustr; + case ocMissing: + return u"missing"_ustr; + case ocBad: + return u"bad"_ustr; + case ocErrRef: + return u"#REF!"_ustr; + default: + return u"?"_ustr; + } + } +}; + +CPPUNIT_TEST_FIXTURE(OOXMLRewriterTest, testSpillBecomesACallInFrontOfItsOperand) +{ + // A1# -> _xlfn.ANCHORARRAY(A1). The operator moves and gets an opcode of its own, so + // nothing later has to tell the two spellings apart by position. + FormulaTokenArray aTokens; + addOperand(aTokens); + aTokens.AddOpCode(ocSpill); + + const std::unique_ptr pRewritten = rewrite(aTokens); + + CPPUNIT_ASSERT(pRewritten); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.ANCHORARRAY ( operand )"_ustr, describe(*pRewritten)); +} + +CPPUNIT_TEST_FIXTURE(OOXMLRewriterTest, testSingleValueKeepsItsPlaceAndGainsParentheses) +{ + // =@A1 -> _xlfn.SINGLE(A1). The @ is already in front, so only the parentheses are new. + FormulaTokenArray aTokens; + aTokens.AddOpCode(ocSingleValue); + addOperand(aTokens); + + const std::unique_ptr pRewritten = rewrite(aTokens); + + CPPUNIT_ASSERT(pRewritten); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE ( operand )"_ustr, describe(*pRewritten)); +} + +CPPUNIT_TEST_FIXTURE(OOXMLRewriterTest, testAnOperandInParenthesesKeepsTheOnesItHas) +{ + // =@(A1) comes out like =@A1 - the parentheses already there serve, no second pair. + FormulaTokenArray aTokens; + aTokens.AddOpCode(ocSingleValue); + aTokens.AddOpCode(ocOpen); + addOperand(aTokens); + aTokens.AddOpCode(ocClose); + + const std::unique_ptr pRewritten = rewrite(aTokens); + + CPPUNIT_ASSERT(pRewritten); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE ( operand )"_ustr, describe(*pRewritten)); +} + +CPPUNIT_TEST_FIXTURE(OOXMLRewriterTest, testUnionGainsParenthesesButKeepsItsOpcode) +{ + // A1~B1 -> (A1,B1). ocUnion already spells as a comma, so only the parentheses are new. + FormulaTokenArray aTokens; + addOperand(aTokens); + aTokens.AddOpCode(ocUnion); + addOperand(aTokens); + + const std::unique_ptr pRewritten = rewrite(aTokens); + + CPPUNIT_ASSERT(pRewritten); + CPPUNIT_ASSERT_EQUAL(u"( operand ~ operand )"_ustr, describe(*pRewritten)); +} + +CPPUNIT_TEST_FIXTURE(OOXMLRewriterTest, testSpillInsideSingleValueNestsTheTwoCalls) +{ + // =@A1# -> _xlfn.SINGLE(_xlfn.ANCHORARRAY(A1)), the @ taking in the whole of the #. + FormulaTokenArray aTokens; + aTokens.AddOpCode(ocSingleValue); + addOperand(aTokens); + aTokens.AddOpCode(ocSpill); + + const std::unique_ptr pRewritten = rewrite(aTokens); + + CPPUNIT_ASSERT(pRewritten); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE ( _xlfn.ANCHORARRAY ( operand ) )"_ustr, + describe(*pRewritten)); +} + +CPPUNIT_TEST_FIXTURE(OOXMLRewriterTest, testSingleValueClosesBeforeAnArithmeticOperator) +{ + // =@A1+B1 -> _xlfn.SINGLE(A1)+B1. The plus is above the @, so only A1 is under it. + FormulaTokenArray aTokens; + aTokens.AddOpCode(ocSingleValue); + addOperand(aTokens); + aTokens.AddOpCode(ocAdd); + addOperand(aTokens); + + const std::unique_ptr pRewritten = rewrite(aTokens); + + CPPUNIT_ASSERT(pRewritten); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE ( operand ) + operand"_ustr, describe(*pRewritten)); +} + +CPPUNIT_TEST_FIXTURE(OOXMLRewriterTest, testLiteralFirstArgumentOfOffsetBecomesAnError) +{ + // OFFSET needs a reference in its first argument, so a literal there is swapped for + // #REF!, which is all any reader accepts. + FormulaTokenArray aTokens; + aTokens.AddOpCode(ocOffset); + aTokens.AddOpCode(ocOpen); + aTokens.AddDouble(1.0); + aTokens.AddOpCode(ocClose); + + const std::unique_ptr pRewritten = rewrite(aTokens); + + CPPUNIT_ASSERT(pRewritten); + CPPUNIT_ASSERT_EQUAL(u"OFFSET ( #REF! )"_ustr, describe(*pRewritten)); +} + +CPPUNIT_TEST_FIXTURE(OOXMLRewriterTest, testAUnionAlreadyInParenthesesNeedsNoRewrite) +{ + // =(A1~B1) already has the parentheses OOXML wants, so there is nothing to change. + FormulaTokenArray aTokens; + aTokens.AddOpCode(ocOpen); + addOperand(aTokens); + aTokens.AddOpCode(ocUnion); + addOperand(aTokens); + aTokens.AddOpCode(ocClose); + + CPPUNIT_ASSERT(!rewrite(aTokens)); +} + +CPPUNIT_TEST_FIXTURE(OOXMLRewriterTest, testAnAtWithNothingToEncloseIsDropped) +{ + // The call name comes from the wrapper, so an @ that got none has to go rather than write + // _xlfn.SINGLE on its own. + FormulaTokenArray aAlone; + aAlone.AddOpCode(ocSingleValue); + + const std::unique_ptr pAlone = rewrite(aAlone); + + CPPUNIT_ASSERT(pAlone); + CPPUNIT_ASSERT_EQUAL(OUString(), describe(*pAlone)); + + // The operand of the first @ is the second one, which writes nothing either. + FormulaTokenArray aNested; + aNested.AddOpCode(ocSingleValue); + aNested.AddOpCode(ocSingleValue); + + const std::unique_ptr pNested = rewrite(aNested); + + CPPUNIT_ASSERT(pNested); + CPPUNIT_ASSERT_EQUAL(OUString(), describe(*pNested)); + + // A missing argument is a token, but it writes no text, so it fills no operand. + FormulaTokenArray aMissing; + aMissing.AddOpCode(ocSingleValue); + aMissing.AddOpCode(ocMissing); + + const std::unique_ptr pMissing = rewrite(aMissing); + + CPPUNIT_ASSERT(pMissing); + CPPUNIT_ASSERT_EQUAL(u"missing"_ustr, describe(*pMissing)); +} + +CPPUNIT_TEST_FIXTURE(OOXMLRewriterTest, testTextThatDidNotParseTakesNoWrapper) +{ + // Parentheses cannot go around text that never parsed, so a wrapper waiting on it is + // dropped and the operator it would have named goes with it. A prefix sign stays. + FormulaTokenArray aSigned; + aSigned.AddOpCode(ocSingleValue); + aSigned.AddOpCode(ocAdd); + aSigned.AddBad(u"PW value"_ustr); + + const std::unique_ptr pSigned = rewrite(aSigned); + + CPPUNIT_ASSERT(pSigned); + CPPUNIT_ASSERT_EQUAL(u"+ bad"_ustr, describe(*pSigned)); + + // Both @ wrappers go at once. + FormulaTokenArray aNested; + aNested.AddOpCode(ocSingleValue); + aNested.AddOpCode(ocSingleValue); + aNested.AddBad(u"PW value"_ustr); + + const std::unique_ptr pNested = rewrite(aNested); + + CPPUNIT_ASSERT(pNested); + CPPUNIT_ASSERT_EQUAL(u"bad"_ustr, describe(*pNested)); + + // A # behind such text has nothing to bind to either. + FormulaTokenArray aSpilled; + aSpilled.AddOpCode(ocSingleValue); + aSpilled.AddBad(u"PW value"_ustr); + aSpilled.AddOpCode(ocSpill); + + const std::unique_ptr pSpilled = rewrite(aSpilled); + + CPPUNIT_ASSERT(pSpilled); + CPPUNIT_ASSERT_EQUAL(u"bad"_ustr, describe(*pSpilled)); +} + +CPPUNIT_TEST_FIXTURE(OOXMLRewriterTest, testAUnionReachingSuchTextNeedsNoRewrite) +{ + // The list loses its parentheses for the same reason. With nothing else to change and no + // token to drop, there is no rewrite at all. + FormulaTokenArray aTokens; + addOperand(aTokens); + aTokens.AddOpCode(ocUnion); + aTokens.AddBad(u"PW value"_ustr); + + CPPUNIT_ASSERT(!rewrite(aTokens)); +} + +CPPUNIT_TEST_FIXTURE(OOXMLRewriterTest, testAnExpressionThatNeedsNoRewrite) +{ + // A1+B1 reads the same in both grammars. + FormulaTokenArray aTokens; + addOperand(aTokens); + aTokens.AddOpCode(ocAdd); + addOperand(aTokens); + + CPPUNIT_ASSERT(!rewrite(aTokens)); +} +} + +CPPUNIT_PLUGIN_IMPLEMENT(); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/formula/source/core/api/FormulaCompiler.cxx b/formula/source/core/api/FormulaCompiler.cxx index e661571b257b..c4bf99ac64f9 100644 --- a/formula/source/core/api/FormulaCompiler.cxx +++ b/formula/source/core/api/FormulaCompiler.cxx @@ -27,6 +27,9 @@ #include #include #include +#include + +#include #include #include @@ -40,7 +43,6 @@ #include #include #include -#include namespace formula { @@ -2809,211 +2811,6 @@ void FormulaCompiler::CreateStringFromTokenArray( OUString& rFormula ) rFormula = aBuffer.makeStringAndClear(); } -namespace -{ -// Operators that can stand in front of an operand. A sign in that position is -// unary, whichever of the two opcodes the parser gave it. The @ and the # get -// wrappers of their own, so they stay out. -bool lclIsPrefixOperator(OpCode eOp) -{ - if (eOp == ocSingleValue || eOp == ocSpill) - return false; - return eOp == ocAdd || eOp == ocSub || isUnaryOperatorOpCode(eOp); -} - -bool lclIsOoxmlFactorSeparator(OpCode eOp) -{ - // Tokens that stand between two factors at the same parenthesis depth, so an - // operand starts behind them. - if (isBinaryOperatorOpCode(eOp)) - return true; - return eOp == ocSep || eOp == ocArrayColSep || eOp == ocArrayRowSep; -} - -// AND, OR and the call operator have operator opcodes but are written as a -// call, so the parenthesis behind the name opens an argument list, not a group. -bool lclIsCallBeforeArguments(OpCode eOp, const FormulaTokenArrayPlainIterator& rIterator) -{ - if (eOp != ocAnd && eOp != ocOr && eOp != ocCall) - return false; - const FormulaToken* pNext = rIterator.PeekNextNoSpaces(); - return pNext && pNext->GetOpCode() == ocOpen; -} - -// Tokens that end the operand of an @ in front of them. Range, intersection, -// union and call bind tighter than the @ and stay inside. Everything above it -// in the grammar ends it. -bool lclEndsSingleValueOperand(OpCode eOp) -{ - if (eOp == ocRange || eOp == ocIntersect || eOp == ocUnion || eOp == ocCall) - return false; - if (isBinaryOperatorOpCode(eOp)) - return true; - return eOp == ocSep || eOp == ocArrayColSep || eOp == ocArrayRowSep - || eOp == ocPercentSign; -} - -// An @ whose wrapper is still to go in. -struct PendingSingleValue -{ - // The parenthesis scope its operand lives in, 1 for the outermost level. - sal_Int32 mnScope; - // Where the wrapper begins, as an offset into the formula text written so far. - sal_Int32 mnWrapStart; - // Whether text for the operand stands in the buffer. - bool mbHasOperand; -}; - -// Mark the pending wrappers' operands as written. Marks never come off, so -// the walk from the back can stop at the first marked one. -void lclMarkPendingOperand(std::vector& rPending) -{ - for (PendingSingleValue& rSingle : std::views::reverse(rPending)) - { - if (rSingle.mbHasOperand) - break; - rSingle.mbHasOperand = true; - } -} - -// Buffer positions for one open parenthesis scope, as offsets into the formula -// text written so far. -struct UnionScope -{ - // Where the scope's own text begins. - sal_Int32 mnContentStart; - // Where the union-level expression being written began. - sal_Int32 mnExpressionStart; - // Where an open union list began. -1 while the scope has none. - sal_Int32 mnListStart; - // The scope is a grouping parenthesis rather than a call's argument list. - bool mbGroupParenthesis; -}; - -// Whether the text from nStart to the end is one parenthesised group, so its -// parentheses can double as the wrapper's. Quoted literals are skipped. A -// doubled quote toggles the quote state twice, which cancels out. -bool lclIsOneParenthesizedGroup(const OUStringBuffer& rBuffer, sal_Int32 nStart) -{ - const sal_Int32 nEnd = rBuffer.getLength(); - if (nEnd - nStart < 2 || rBuffer[nStart] != '(' || rBuffer[nEnd - 1] != ')') - return false; - sal_Int32 nGroupDepth = 0; - // The braces of an inline matrix and the brackets of a table or external - // reference, which carry separators of their own. - sal_Int32 nBracketDepth = 0; - sal_Unicode cQuote = 0; - for (sal_Int32 nIndex = nStart; nIndex < nEnd; ++nIndex) - { - const sal_Unicode cChar = rBuffer[nIndex]; - if (cQuote != 0) - { - if (cChar == cQuote) - cQuote = 0; - } - else if (cChar == '"' || cChar == '\'') - cQuote = cChar; - else if (cChar == '{' || cChar == '[') - ++nBracketDepth; - else if (cChar == '}' || cChar == ']') - --nBracketDepth; - else if (cChar == '(') - ++nGroupDepth; - else if (cChar == ')') - { - --nGroupDepth; - if (nGroupDepth == 0) - return nIndex == nEnd - 1; - } - else if (cChar == ',' && nGroupDepth == 1 && nBracketDepth == 0) - { - // A comma directly inside the group makes it a union list, whose - // parentheses keep it one argument, so they cannot double as the - // wrapper's own. - return false; - } - } - return false; -} - -// Splice in the wrappers pending from nScope and deeper, innermost first. -// Each operand runs from its recorded start to the buffer's end. -void lclCloseSingleValues(OUStringBuffer& rBuffer, UnionScope& rScope, - std::vector& rPending, sal_Int32 nScope) -{ - // The wrappers stand in the order their operands begin, so splicing the one at - // the back in leaves the starts still waiting where they are. - assert(std::ranges::is_sorted(rPending, {}, &PendingSingleValue::mnWrapStart)); - while (!rPending.empty() && rPending.back().mnScope >= nScope) - { - const PendingSingleValue& rSingle = rPending.back(); - // An operand that stayed empty takes no wrapper. - if (rSingle.mbHasOperand) - { - if (lclIsOneParenthesizedGroup(rBuffer, rSingle.mnWrapStart)) - rBuffer.insert(rSingle.mnWrapStart, u"_xlfn.SINGLE"); - else - { - rBuffer.insert(rSingle.mnWrapStart, u"_xlfn.SINGLE("); - rBuffer.append(u")"); - } - // The wrapper encloses the expression, which now begins at it. - if (rScope.mnExpressionStart > rSingle.mnWrapStart) - rScope.mnExpressionStart = rSingle.mnWrapStart; - } - rPending.pop_back(); - } -} - -// Drop the wrappers pending from nScope and deeper without splicing them in. -void lclCancelSingleValues(std::vector& rPending, sal_Int32 nScope) -{ - while (!rPending.empty() && rPending.back().mnScope >= nScope) - rPending.pop_back(); -} - -// Whether a wrapper still to be spliced in begins where the scope's text does or -// ahead of it, so the scope's own parentheses are already spoken for. -bool lclHasWrapperAtStart(const std::vector& rPending, sal_Int32 nScope, - sal_Int32 nContentStart) -{ - for (const PendingSingleValue& rSingle : std::views::reverse(rPending)) - { - if (rSingle.mnScope < nScope) - break; - if (rSingle.mnWrapStart <= nContentStart) - return true; - } - return false; -} - -// Wrap the scope's open union list in parentheses. A list that exactly fills a -// grouping parenthesis keeps that pair when bMayTakeScopeParenthesis allows it. -void lclCloseUnionList(OUStringBuffer& rBuffer, UnionScope& rScope, - std::vector& rPending, bool bMayTakeScopeParenthesis) -{ - if (rScope.mnListStart < 0) - return; - if (!(bMayTakeScopeParenthesis && rScope.mbGroupParenthesis - && rScope.mnListStart == rScope.mnContentStart)) - { - rBuffer.insert(rScope.mnListStart, u"("); - rBuffer.append(u")"); - // The opening parenthesis moves the text behind it along, so a wrapper - // waiting inside the list follows its own operand. - for (PendingSingleValue& rSingle : rPending) - { - if (rSingle.mnWrapStart > rScope.mnListStart) - ++rSingle.mnWrapStart; - } - // An expression that began inside the list moves along with it. - if (rScope.mnExpressionStart > rScope.mnListStart) - ++rScope.mnExpressionStart; - } - rScope.mnListStart = -1; -} -} // namespace - bool FormulaCompiler::AppendTokenOrError(OUStringBuffer& rBuffer, const FormulaToken*& rpToken) { const OpCode eOp = rpToken->GetOpCode(); @@ -3080,8 +2877,16 @@ void FormulaCompiler::CreateStringFromTokenArray( OUStringBuffer& rBuffer ) { MissingConventionOOXML aConv; mpArr = mpArr->RewriteMissing( aConv ); - maArrIterator = FormulaTokenArrayPlainIterator( *mpArr ); } + // Put in what OOXML spells out, so writing is a plain walk over the tokens. + OOXMLRewriter aRewriter(*mpArr); + if (std::unique_ptr pOoxml = aRewriter.releaseTokens()) + { + if (mpArr != pSaveArr) + delete mpArr; + mpArr = pOoxml.release(); + } + maArrIterator = FormulaTokenArrayPlainIterator( *mpArr ); } // At least one character per token, plus some are references, some are @@ -3091,225 +2896,13 @@ void FormulaCompiler::CreateStringFromTokenArray( OUStringBuffer& rBuffer ) if ( mpArr->IsRecalcModeForced() ) rBuffer.append( '='); - // The @ markers whose _xlfn.SINGLE wrapper is still to be spliced in. - std::vector aPendingSingles; - // One entry per parenthesis scope, tracking where the scope's expressions - // begin and where its union list does. Start past the optional leading '='. - std::vector aUnionScopes; - aUnionScopes.push_back({ rBuffer.getLength(), rBuffer.getLength(), -1, false }); - // Whether the next token stands where an operand starts, so a plus or a - // minus there is the unary form. - bool bOperandPosition = true; - // Whether the current factor is raw text that never parsed, which no - // wrapper's parentheses can go around. - bool bBadFactor = false; - bool bUnwritable = false; const FormulaToken* t = maArrIterator.First(); while( t ) { - if (FormulaGrammar::isOOXML(meGrammar) && t->GetOpCode() == ocOffset - && t->GetType() == svByte) - { - FormulaTokenArrayPlainIterator aTempIter(*mpArr); - aTempIter.Jump(maArrIterator.GetIndex()); - FormulaToken* pNext = aTempIter.Next(); - if (pNext && pNext->GetOpCode() == ocOpen - && pNext->GetType() == svSep) - { - FormulaToken* pNext2 = aTempIter.Next(); - if (pNext2 && pNext2->GetOpCode() == ocPush) - { - StackVar eType = pNext2->GetType(); - if (eType == svString || eType == svDouble) - { - rBuffer.append(GetNativeSymbol(t->GetOpCode())); - rBuffer.append(GetNativeSymbol(pNext->GetOpCode())); - // The consumed parenthesis opens a scope like a written one. - lclMarkPendingOperand(aPendingSingles); - aUnionScopes.push_back( - { rBuffer.getLength(), rBuffer.getLength(), -1, false }); - rBuffer.append(GetNativeSymbol(ocErrRef)); - bOperandPosition = false; - bBadFactor = false; - maArrIterator.Jump(aTempIter.GetIndex()); - t = maArrIterator.Next(); - continue; - } - } - } - } - - // The @ writes no text of its own. Remember where its wrapper goes and - // the scope its operand lives in. The wrapper is spliced in once the - // operand stands in the buffer. - if (FormulaGrammar::isOOXML(meGrammar) && t->GetOpCode() == ocSingleValue) - { - aPendingSingles.push_back({ sal_Int32(aUnionScopes.size()), - rBuffer.getLength(), false }); - t = maArrIterator.Next(); - continue; - } - - // XLSX writes the # operator as _xlfn.ANCHORARRAY around the whole - // expression under it, so the union list closes first. Text that - // never parsed takes no parentheses, so a # behind it is dropped. - if (FormulaGrammar::isOOXML(meGrammar) && t->GetOpCode() == ocSpill) - { - if (!bBadFactor) - { - lclCloseUnionList(rBuffer, aUnionScopes.back(), aPendingSingles, false); - const sal_Int32 nWrapStart = aUnionScopes.back().mnExpressionStart; - rBuffer.insert(nWrapStart, u"_xlfn.ANCHORARRAY("); - rBuffer.append(u")"); - // The # binds tighter than an @ in front of it, so an @ that - // begins inside the wrapper takes the whole of it as its operand. - // An @ on one part of a union list is such an @. - for (PendingSingleValue& rSingle : aPendingSingles) - { - if (rSingle.mnWrapStart > nWrapStart) - rSingle.mnWrapStart = nWrapStart; - } - lclMarkPendingOperand(aPendingSingles); - } - t = maArrIterator.Next(); - bOperandPosition = false; - continue; - } - - if (FormulaGrammar::isOOXML(meGrammar)) - { - // ocOpen pushes a nested scope and ocClose pops it. Inside a scope the - // walk tracks where the expression under the union operator begins. - const OpCode eOpHere = t->GetOpCode(); - const bool bWhitespace = isWhitespaceOpCode(eOpHere); - // A sign where an operand starts can only be the unary form, - // whichever opcode the parser gave it, and belongs to the operand - // behind it. - const bool bPrefix = bOperandPosition && lclIsPrefixOperator(eOpHere); - const bool bCallName = !bPrefix && lclIsCallBeforeArguments(eOpHere, maArrIterator); - const bool bSeparator - = !bPrefix && !bCallName && lclIsOoxmlFactorSeparator(eOpHere); - // The union list and the @ operand cover the same stretch of the - // grammar, so the same tokens end both of them. - const bool bEndsOperand - = !bPrefix && !bCallName && lclEndsSingleValueOperand(eOpHere); - - // A structured table reference walks through here token by token, - // so its brackets scope like parentheses and the separators inside - // them stay inside the reference. - if (eOpHere == ocOpen || eOpHere == ocTableRefOpen) - { - // Leave the outer expression start alone: a separator already - // marked a grouping '(', and a call is wrapped from its name. - if (!AppendTokenOrError(rBuffer, t)) - { - bUnwritable = true; - break; - } - lclMarkPendingOperand(aPendingSingles); - // A grouping parenthesis stands where an operand starts; a call's - // follows its function name. - aUnionScopes.push_back({ rBuffer.getLength(), rBuffer.getLength(), -1, - eOpHere == ocOpen && bOperandPosition }); - bOperandPosition = true; - bBadFactor = false; - continue; - } - if (eOpHere == ocClose || eOpHere == ocTableRefClose) - { - if (aUnionScopes.size() > 1) - { - // The union list closes before the @ wrappers, so its - // parentheses sit inside theirs. - const sal_Int32 nScope = sal_Int32(aUnionScopes.size()); - const bool bMayTakeScopeParenthesis = !lclHasWrapperAtStart( - aPendingSingles, nScope, aUnionScopes.back().mnContentStart); - lclCloseUnionList(rBuffer, aUnionScopes.back(), aPendingSingles, - bMayTakeScopeParenthesis); - lclCloseSingleValues(rBuffer, aUnionScopes.back(), aPendingSingles, nScope); - } - if (!AppendTokenOrError(rBuffer, t)) - { - bUnwritable = true; - break; - } - if (aUnionScopes.size() > 1) - aUnionScopes.pop_back(); - lclMarkPendingOperand(aPendingSingles); - bOperandPosition = false; - bBadFactor = false; - continue; - } - - // An operator the grammar puts above them ends the union list and the - // @ operand, so their wrappers close in front of it. - if (bEndsOperand) - { - lclCloseUnionList(rBuffer, aUnionScopes.back(), aPendingSingles, false); - lclCloseSingleValues(rBuffer, aUnionScopes.back(), aPendingSingles, - sal_Int32(aUnionScopes.size())); - } - - if (eOpHere == ocBad) - { - // Raw text that no parse could read cannot take parentheses, so - // the wrappers waiting on it are left out. - aUnionScopes.back().mnListStart = -1; - lclCancelSingleValues(aPendingSingles, sal_Int32(aUnionScopes.size())); - bBadFactor = true; - } - - const sal_Int32 nLengthBefore = rBuffer.getLength(); - // Whitespace ahead of a scope or of an expression is not part of it, - // so the start it is measured from moves past the whitespace. - const bool bAtContentStart - = bWhitespace && nLengthBefore == aUnionScopes.back().mnContentStart; - const bool bAtExpressionStart - = bWhitespace && nLengthBefore == aUnionScopes.back().mnExpressionStart; - if (!AppendTokenOrError(rBuffer, t)) - { - bUnwritable = true; - break; - } - if (!bPrefix && !bWhitespace && rBuffer.getLength() > nLengthBefore) - lclMarkPendingOperand(aPendingSingles); - if (bAtContentStart) - aUnionScopes.back().mnContentStart = rBuffer.getLength(); - if (bAtExpressionStart) - aUnionScopes.back().mnExpressionStart = rBuffer.getLength(); - if (eOpHere == ocUnion && aUnionScopes.back().mnListStart < 0) - { - // The first union operator of the expression opens the list; the - // ones after it keep extending the same list. - aUnionScopes.back().mnListStart = aUnionScopes.back().mnExpressionStart; - } - if (bPrefix || (bEndsOperand && bSeparator)) - { - // The next expression begins behind a prefix sign or an infix - // operator. A postfix one follows its operand instead. - aUnionScopes.back().mnExpressionStart = rBuffer.getLength(); - } - if (bSeparator) - bBadFactor = false; - if (!bWhitespace) - bOperandPosition = bPrefix || bSeparator; - continue; - } - if (!AppendTokenOrError(rBuffer, t)) break; } - // Whatever is still pending wraps up to the end of the formula, unless the - // formula came out as a single error string. The union lists close first, so - // their parentheses sit inside the @ wrappers. - if (FormulaGrammar::isOOXML(meGrammar) && !bUnwritable) - { - for (UnionScope& rScope : std::views::reverse(aUnionScopes)) - lclCloseUnionList(rBuffer, rScope, aPendingSingles, false); - lclCloseSingleValues(rBuffer, aUnionScopes.front(), aPendingSingles, 1); - } - if (pSaveArr != mpArr) { delete mpArr; diff --git a/formula/source/core/api/OOXMLRewriter.cxx b/formula/source/core/api/OOXMLRewriter.cxx new file mode 100644 index 000000000000..0d215feed790 --- /dev/null +++ b/formula/source/core/api/OOXMLRewriter.cxx @@ -0,0 +1,534 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * Copyright the LibreOffice contributors. + * + * SPDX-License-Identifier: MPL-2.0 + * + * 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/. + */ + +#include + +#include +#include + +#include +#include +#include +#include + +namespace formula +{ +namespace +{ +const sal_uInt16 NO_POSITION = SAL_MAX_UINT16; + +// Tokens the OOXML shape has no place for: the call name of the wrapper around their operand +// stands for them. +bool lclIsDropped(OpCode eOp) { return eOp == ocSingleValue || eOp == ocSpill; } + +// Tokens that write no text, so an operand made of nothing else leaves its wrapper with +// nothing to enclose and the wrapper goes. +bool lclWritesNoText(OpCode eOp) { return lclIsDropped(eOp) || eOp == ocMissing; } + +// A parenthesis pair to put around a token range, with a call name before it. What the first +// pass hands the second. +struct OOXMLWrapper +{ + // Half-open range of token positions. + sal_uInt16 mnBegin; + sal_uInt16 mnEnd; + // Call name before the opening parenthesis, ocNone for a bare pair. + OpCode meOperator; + // False when the range already is a parenthesised group, so its own pair serves. + bool mbOwnParentheses; +}; + +// Operators that can come before an operand. A sign there is always the unary form, no +// matter which of the two opcodes the parser used. @ and # are left out, they get their own +// wrappers. +bool lclIsPrefixOperator(OpCode eOp) +{ + if (eOp == ocSingleValue || eOp == ocSpill) + return false; + return eOp == ocAdd || eOp == ocSub || isUnaryOperatorOpCode(eOp); +} + +bool lclIsFactorSeparator(OpCode eOp) +{ + // Tokens between two factors at the same parenthesis depth - a new operand starts after + // them. + if (isBinaryOperatorOpCode(eOp)) + return true; + return eOp == ocSep || eOp == ocArrayColSep || eOp == ocArrayRowSep; +} + +// Tokens that end the operand of a preceding @. Range, intersection, union and call bind +// tighter than the @ so they stay inside, everything above it in the grammar ends it. +bool lclEndsSingleValueOperand(OpCode eOp) +{ + if (eOp == ocRange || eOp == ocIntersect || eOp == ocUnion || eOp == ocCall) + return false; + return lclIsFactorSeparator(eOp) || eOp == ocPercentSign; +} + +// Single pass over the token array to collect what the OOXML spelling needs. +// +// Precedence comes from the order the tokens appear in, so this is a second statement of the +// precedence the parser already applied. Change how an operator binds in one place and the +// other one needs the same change. +class RewriteCollector +{ +public: + RewriteCollector(const FormulaTokenArray& rTokens, std::vector& rWrappers, + std::vector& rErrorPositions) + : mrTokens(rTokens) + , mrWrappers(rWrappers) + , mrErrorPositions(rErrorPositions) + { + } + + void collect(); + +private: + // An @ we have not recorded a wrapper for yet. + struct PendingSingleValue + { + // Parenthesis scope the operand is in, 1 is the outermost. + size_t mnScope; + // First token position of the operand. + sal_uInt16 mnBegin; + // Set once a token that actually carries an operand shows up in the range. + bool mbHasOperand; + }; + + // Positions tracked for one open parenthesis scope. + struct Scope + { + // First token position inside the scope. + sal_uInt16 mnContentStart; + // Start of the union-level expression we are reading. + sal_uInt16 mnExpressionStart; + // Start of an open union list, NO_POSITION if there is none. + sal_uInt16 mnListStart; + // The scope is a grouping parenthesis, not a call's argument list. + bool mbGroupParenthesis; + }; + + OpCode opCodeAt(sal_uInt16 nPosition) const; + sal_uInt16 literalOffsetArgument(sal_uInt16 nPosition) const; + bool isCallBeforeArguments(OpCode eOp, sal_uInt16 nPosition) const; + bool isOneParenthesizedGroup(sal_uInt16 nBegin, sal_uInt16 nEnd) const; + bool hasPendingAtStart(sal_uInt16 nContentStart) const; + + void markPendingOperand(); + static void movePastToken(Scope& rScope, sal_uInt16 nPosition); + void closeSingleValues(size_t nScope, sal_uInt16 nEnd); + void cancelSingleValues(size_t nScope); + void closeUnionList(sal_uInt16 nEnd, bool bMayTakeScopeParenthesis); + + const FormulaTokenArray& mrTokens; + std::vector& mrWrappers; + std::vector& mrErrorPositions; + std::vector maPendingSingles; + std::vector maScopes; +}; + +OpCode RewriteCollector::opCodeAt(sal_uInt16 nPosition) const +{ + return mrTokens.TokenAt(nPosition)->GetOpCode(); +} + +// Position of the first argument if the token at nPosition opens an OFFSET() call with a +// literal string or number there, NO_POSITION otherwise. OFFSET() needs a reference, so a +// literal cannot be written out in a way any reader accepts. +sal_uInt16 RewriteCollector::literalOffsetArgument(sal_uInt16 nPosition) const +{ + if (nPosition + 2 >= mrTokens.GetLen()) + return NO_POSITION; + const FormulaToken* pName = mrTokens.TokenAt(nPosition); + if (pName->GetOpCode() != ocOffset || pName->GetType() != svByte) + return NO_POSITION; + const FormulaToken* pOpen = mrTokens.TokenAt(sal_uInt16(nPosition + 1)); + if (pOpen->GetOpCode() != ocOpen || pOpen->GetType() != svSep) + return NO_POSITION; + const FormulaToken* pArgument = mrTokens.TokenAt(sal_uInt16(nPosition + 2)); + if (pArgument->GetOpCode() != ocPush) + return NO_POSITION; + const StackVar eType = pArgument->GetType(); + if (eType != svString && eType != svDouble) + return NO_POSITION; + return sal_uInt16(nPosition + 2); +} + +// AND, OR and the call operator have operator opcodes but are written as calls, so the +// parenthesis after the name opens an argument list, not a group. +bool RewriteCollector::isCallBeforeArguments(OpCode eOp, sal_uInt16 nPosition) const +{ + if (eOp != ocAnd && eOp != ocOr && eOp != ocCall) + return false; + for (sal_uInt16 nNext = sal_uInt16(nPosition + 1); nNext < mrTokens.GetLen(); ++nNext) + { + const OpCode eNext = opCodeAt(nNext); + if (!isWhitespaceOpCode(eNext)) + return eNext == ocOpen; + } + return false; +} + +// The range is a single parenthesised group, so we can reuse its parentheses for the +// wrapper. +bool RewriteCollector::isOneParenthesizedGroup(sal_uInt16 nBegin, sal_uInt16 nEnd) const +{ + if (nEnd - nBegin < 2 || opCodeAt(nBegin) != ocOpen) + return false; + sal_Int32 nDepth = 0; + for (sal_uInt16 nPosition = nBegin; nPosition < nEnd; ++nPosition) + { + // Inline matrix braces and table reference brackets have separators of their own, + // so count them as scopes too. + const OpCode eOp = opCodeAt(nPosition); + if (eOp == ocOpen || eOp == ocArrayOpen || eOp == ocTableRefOpen) + { + ++nDepth; + } + else if (eOp == ocClose || eOp == ocArrayClose || eOp == ocTableRefClose) + { + --nDepth; + if (nDepth == 0) + return nPosition == nEnd - 1; + } + else if (nDepth == 1 && (eOp == ocUnion || eOp == ocSep)) + { + // A separator directly inside means the parentheses hold arguments together, + // so we cannot reuse them. + return false; + } + } + return false; +} + +// A pending operand starts at or before the scope's own tokens, so the scope's parentheses +// are already taken. +bool RewriteCollector::hasPendingAtStart(sal_uInt16 nContentStart) const +{ + for (const PendingSingleValue& rSingle : maPendingSingles | std::views::reverse) + { + if (rSingle.mnScope < maScopes.size()) + break; + if (rSingle.mnBegin <= nContentStart) + return true; + } + return false; +} + +// A token that writes no text cannot be the start of anything, so move any start that sits +// on it forward. +void RewriteCollector::movePastToken(Scope& rScope, sal_uInt16 nPosition) +{ + if (rScope.mnContentStart == nPosition) + rScope.mnContentStart = sal_uInt16(nPosition + 1); + if (rScope.mnExpressionStart == nPosition) + rScope.mnExpressionStart = sal_uInt16(nPosition + 1); +} + +// Mark pending operands as filled. A mark is never cleared, so walking from the back can +// stop at the first marked one. +void RewriteCollector::markPendingOperand() +{ + for (PendingSingleValue& rSingle : maPendingSingles | std::views::reverse) + { + if (rSingle.mbHasOperand) + break; + rSingle.mbHasOperand = true; + } +} + +// Record the wrappers pending from nScope and deeper, innermost first. Each operand runs +// from its own start to nEnd. +void RewriteCollector::closeSingleValues(size_t nScope, sal_uInt16 nEnd) +{ + while (!maPendingSingles.empty() && maPendingSingles.back().mnScope >= nScope) + { + const PendingSingleValue& rSingle = maPendingSingles.back(); + // An empty operand gets no wrapper. + if (rSingle.mbHasOperand) + { + mrWrappers.push_back({ rSingle.mnBegin, nEnd, ocSingleValue, + !isOneParenthesizedGroup(rSingle.mnBegin, nEnd) }); + // The wrapper now encloses the expression, so the expression starts at the + // wrapper. Whitespace the scope start had skipped is inside the wrapper too, + // so pull that start back as well. + if (maScopes.back().mnExpressionStart > rSingle.mnBegin) + maScopes.back().mnExpressionStart = rSingle.mnBegin; + if (maScopes.back().mnContentStart > rSingle.mnBegin) + maScopes.back().mnContentStart = rSingle.mnBegin; + } + maPendingSingles.pop_back(); + } +} + +// Throw away the wrappers pending from nScope and deeper. +void RewriteCollector::cancelSingleValues(size_t nScope) +{ + while (!maPendingSingles.empty() && maPendingSingles.back().mnScope >= nScope) + maPendingSingles.pop_back(); +} + +// Record the parentheses for the scope's open union list. If the list exactly fills a +// grouping parenthesis and bMayTakeScopeParenthesis allows it, reuse that pair. +void RewriteCollector::closeUnionList(sal_uInt16 nEnd, bool bMayTakeScopeParenthesis) +{ + Scope& rScope = maScopes.back(); + if (rScope.mnListStart == NO_POSITION) + return; + if (!(bMayTakeScopeParenthesis && rScope.mbGroupParenthesis + && rScope.mnListStart == rScope.mnContentStart)) + { + mrWrappers.push_back({ rScope.mnListStart, nEnd, ocNone, true }); + } + rScope.mnListStart = NO_POSITION; +} + +void RewriteCollector::collect() +{ + const sal_uInt16 nLength = mrTokens.GetLen(); + maScopes.push_back({ 0, 0, NO_POSITION, false }); + // The next token is where an operand starts, so a plus or minus there is the unary form. + bool bOperandPosition = true; + // The current factor is raw text that never parsed - we cannot put wrapper parentheses + // around that. + bool bBadFactor = false; + + for (sal_uInt16 nPosition = 0; nPosition < nLength; ++nPosition) + { + const OpCode eOp = opCodeAt(nPosition); + + const sal_uInt16 nLiteralOffsetArgument = literalOffsetArgument(nPosition); + if (nLiteralOffsetArgument != NO_POSITION) + mrErrorPositions.push_back(nLiteralOffsetArgument); + + // The @ writes no text of its own. Remember where the operand starts and which scope + // it is in, the wrapper gets recorded once we know how far the operand reaches. + if (eOp == ocSingleValue) + { + Scope& rScope = maScopes.back(); + // Any start sitting on the @ moves on to the operand after it. + movePastToken(rScope, nPosition); + maPendingSingles.push_back({ maScopes.size(), sal_uInt16(nPosition + 1), false }); + continue; + } + + // The # is written as a call around the whole expression under it, so the union list + // closes first. Text that never parsed takes no parentheses, so drop a # after one. + if (eOp == ocSpill) + { + if (!bBadFactor) + { + closeUnionList(nPosition, false); + const sal_uInt16 nBegin = maScopes.back().mnExpressionStart; + mrWrappers.push_back({ nBegin, nPosition, ocAnchorArray, true }); + // The # binds tighter than an @ before it, so an @ that starts inside the + // wrapper takes all of it as its operand. An @ on one part of a union list + // is such a case. + for (PendingSingleValue& rSingle : maPendingSingles) + { + if (rSingle.mnBegin > nBegin) + rSingle.mnBegin = nBegin; + } + markPendingOperand(); + } + bOperandPosition = false; + continue; + } + + const bool bWhitespace = isWhitespaceOpCode(eOp); + // A sign where an operand starts is always the unary form, whichever opcode the + // parser used, and it belongs to the operand after it. + const bool bPrefix = bOperandPosition && lclIsPrefixOperator(eOp); + const bool bCallName = !bPrefix && isCallBeforeArguments(eOp, nPosition); + const bool bSeparator = !bPrefix && !bCallName && lclIsFactorSeparator(eOp); + // The union list and the @ operand cover the same part of the grammar, so the same + // tokens end both. + const bool bEndsOperand = !bPrefix && !bCallName && lclEndsSingleValueOperand(eOp); + + // A structured table reference comes through here token by token, so its brackets + // scope like parentheses and the separators inside stay with the reference. + if (eOp == ocOpen || eOp == ocTableRefOpen) + { + markPendingOperand(); + // A grouping parenthesis is where an operand starts, a call's parenthesis comes + // after the function name. + maScopes.push_back({ sal_uInt16(nPosition + 1), sal_uInt16(nPosition + 1), NO_POSITION, + eOp == ocOpen && bOperandPosition }); + bOperandPosition = true; + bBadFactor = false; + continue; + } + if (eOp == ocClose || eOp == ocTableRefClose) + { + if (maScopes.size() > 1) + { + // The union list closes before the @ wrappers, so its parentheses end up + // inside theirs. + const bool bMayTakeScopeParenthesis + = !hasPendingAtStart(maScopes.back().mnContentStart); + closeUnionList(nPosition, bMayTakeScopeParenthesis); + closeSingleValues(maScopes.size(), nPosition); + maScopes.pop_back(); + } + markPendingOperand(); + bOperandPosition = false; + bBadFactor = false; + continue; + } + + // An operator higher up in the grammar ends both the union list and the @ operand, + // so their wrappers close before it. + if (bEndsOperand) + { + closeUnionList(nPosition, false); + closeSingleValues(maScopes.size(), nPosition); + } + + if (eOp == ocBad) + { + // Raw text that never parsed cannot take parentheses, so drop the wrappers + // waiting on it. + maScopes.back().mnListStart = NO_POSITION; + cancelSingleValues(maScopes.size()); + bBadFactor = true; + } + + Scope& rScope = maScopes.back(); + // Whitespace before a scope or an expression is not part of it. + if (bWhitespace) + movePastToken(rScope, nPosition); + if (!bPrefix && !bWhitespace && !lclWritesNoText(eOp)) + markPendingOperand(); + if (eOp == ocUnion && rScope.mnListStart == NO_POSITION) + { + // The first union operator opens the list, the ones after it just extend it. + rScope.mnListStart = rScope.mnExpressionStart; + } + if (bPrefix || (bEndsOperand && bSeparator)) + { + // A prefix sign or an infix operator starts a new expression after it. A postfix + // one does not, it belongs to the operand before it. + rScope.mnExpressionStart = sal_uInt16(nPosition + 1); + } + if (bSeparator) + bBadFactor = false; + if (!bWhitespace) + bOperandPosition = bPrefix || bSeparator; + } + + // Anything still pending reaches the end of the formula. Union lists close first so + // their parentheses end up inside the @ wrappers. + while (maScopes.size() > 1) + { + closeUnionList(nLength, false); + maScopes.pop_back(); + } + closeUnionList(nLength, false); + closeSingleValues(1, nLength); + + // A wrapper recorded later always reaches at least as far as an earlier one, so sorting + // by end descending puts the wider of two wrappers with the same begin first. The + // reverse before that handles the ones that also end at the same position. + std::reverse(mrWrappers.begin(), mrWrappers.end()); + std::stable_sort(mrWrappers.begin(), mrWrappers.end(), + [](const OOXMLWrapper& rLeft, const OOXMLWrapper& rRight) { + if (rLeft.mnBegin != rRight.mnBegin) + return rLeft.mnBegin < rRight.mnBegin; + return rLeft.mnEnd > rRight.mnEnd; + }); +} + +// Copy rTokens with the recorded call names and parentheses put in, and #REF! for the tokens +// that have no OOXML spelling. +std::unique_ptr lclBuild(const FormulaTokenArray& rTokens, + const std::vector& rWrappers, + const std::vector& rErrorPositions) +{ + auto pRewritten = std::make_unique(); + pRewritten->AddRecalcMode(rTokens.GetRecalcMode()); + + // The end position of every pair opened so far, innermost last. + std::vector aOpenEnds; + size_t nNextWrapper = 0; + const sal_uInt16 nLength = rTokens.GetLen(); + // One past the last token, so the pairs reaching the end get closed. + for (sal_uInt16 nPosition = 0; nPosition <= nLength; ++nPosition) + { + while (!aOpenEnds.empty() && aOpenEnds.back() <= nPosition) + { + pRewritten->AddOpCode(ocClose); + aOpenEnds.pop_back(); + } + while (nNextWrapper < rWrappers.size() && rWrappers[nNextWrapper].mnBegin <= nPosition) + { + const OOXMLWrapper& rWrapper = rWrappers[nNextWrapper++]; + // Nothing between the ends, so there is nothing to enclose and the pair goes. + // A # with no factor in front of it is the case that gets here. + if (rWrapper.mnBegin == rWrapper.mnEnd) + continue; + if (rWrapper.meOperator != ocNone) + pRewritten->AddOpCode(rWrapper.meOperator); + if (rWrapper.mbOwnParentheses) + { + pRewritten->AddOpCode(ocOpen); + aOpenEnds.push_back(rWrapper.mnEnd); + } + } + if (nPosition == nLength) + break; + + const FormulaToken* pToken = rTokens.TokenAt(nPosition); + const OpCode eOp = pToken->GetOpCode(); + if (lclIsDropped(eOp)) + { + // The wrapper's call name stands for these. A # that got no wrapper had + // nothing to bind to, so it goes. + continue; + } + if (std::binary_search(rErrorPositions.begin(), rErrorPositions.end(), nPosition)) + pRewritten->AddOpCode(ocErrRef); + else + pRewritten->AddToken(*pToken); + } + return pRewritten; +} + +// Whether the OOXML shape differs from the array as it stands. Dropping a token counts, so a +// lone @ with no operand is a change even though it takes no wrapper. +bool lclNeedsRewrite(const FormulaTokenArray& rTokens, const std::vector& rWrappers, + const std::vector& rErrorPositions) +{ + if (!rWrappers.empty() || !rErrorPositions.empty()) + return true; + for (sal_uInt16 nPosition = 0; nPosition < rTokens.GetLen(); ++nPosition) + { + if (lclIsDropped(rTokens.TokenAt(nPosition)->GetOpCode())) + return true; + } + return false; +} + +} // namespace + +OOXMLRewriter::OOXMLRewriter(const FormulaTokenArray& rTokens) +{ + std::vector aWrappers; + std::vector aErrorPositions; + RewriteCollector(rTokens, aWrappers, aErrorPositions).collect(); + if (!lclNeedsRewrite(rTokens, aWrappers, aErrorPositions)) + return; + mpRewritten = lclBuild(rTokens, aWrappers, aErrorPositions); +} + +} // namespace formula + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/include/formula/opcode.hxx b/include/formula/opcode.hxx index fa158cb6e62c..499a55f0a565 100644 --- a/include/formula/opcode.hxx +++ b/include/formula/opcode.hxx @@ -230,7 +230,10 @@ enum OpCode : sal_uInt16 ocIsoWeeknum = 178, ocNot = 179, ocNeg = 180, - ocStopOneParameter = 181, + // OOXML spelling of the # operator, a call in front of its operand. Only a + // rewritten token array has this. + ocAnchorArray = 181, + ocStopOneParameter = 182, // Functions with more than one parameter ocStartTwoParameters = 201, diff --git a/sc/qa/unit/subsequent_export_test4.cxx b/sc/qa/unit/subsequent_export_test4.cxx index c4ef4c1e8768..b5ee422e4cc6 100644 --- a/sc/qa/unit/subsequent_export_test4.cxx +++ b/sc/qa/unit/subsequent_export_test4.cxx @@ -1418,6 +1418,31 @@ CPPUNIT_TEST_FIXTURE(ScExportTest4, testSingleOnSpilledRangeXlsxExport) u"_xlfn.SINGLE(_xlfn.ANCHORARRAY(A1))"); } +CPPUNIT_TEST_FIXTURE(ScExportTest4, testParenthesizedSpilledRangeXlsxRoundTrip) +{ + // A # on an operand that is already parenthesised keeps those parentheses inside the + // wrapper, so _xlfn.ANCHORARRAY((A1)), which is what OOXML uses. The import reads that + // back as the postfix (A1)# it came from. + createScDoc(); + ScDocument* pDoc = getScDoc(); + + pDoc->SetFormula(ScAddress(0, 0, 0), u"=SEQUENCE(3)"_ustr, + formula::FormulaGrammar::GRAM_NATIVE); + pDoc->SetFormula(ScAddress(1, 0, 0), u"=(A1)#"_ustr, formula::FormulaGrammar::GRAM_NATIVE); + + save(TestFilter::XLSX); + + xmlDocUniquePtr pSheet = parseExport(u"xl/worksheets/sheet1.xml"_ustr); + CPPUNIT_ASSERT(pSheet); + + assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[1]/x:c[2]/x:f", + u"_xlfn.ANCHORARRAY((A1))"); + + saveAndReload(TestFilter::XLSX); + pDoc = getScDoc(); + CPPUNIT_ASSERT_EQUAL(u"=(A1)#"_ustr, pDoc->GetFormula(1, 0, 0)); +} + CPPUNIT_TEST_FIXTURE(ScExportTest4, testReferenceUnionXlsxRoundTrip) { // OOXML spells the union operator with the same comma that separates diff --git a/sc/qa/unit/ucalc_formula2.cxx b/sc/qa/unit/ucalc_formula2.cxx index a86c4b0343a9..b4e9ac2243d8 100644 --- a/sc/qa/unit/ucalc_formula2.cxx +++ b/sc/qa/unit/ucalc_formula2.cxx @@ -6991,6 +6991,11 @@ CPPUNIT_TEST_FIXTURE(TestFormula2, testImplicitIntersectionTakesOperandWhole) CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(A1:INDEX(B1:B10,5))"_ustr, resolveAndPrintAsOoxml(*m_pDoc, u"A1:INDEX(B1:B10;5)"_ustr)); + // An arithmetic operator is above the @ in the grammar, so it ends the operand and the + // wrapper closes before it. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(A1:A3)+1"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@A1:A3+1"_ustr)); + // Whitespace between a function name and its parenthesis is dropped on save, // and the call stays inside the wrapper as if it had never been there. CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(SUM(A1:A3))"_ustr, @@ -7030,14 +7035,27 @@ CPPUNIT_TEST_FIXTURE(TestFormula2, testImplicitIntersectionMarkersWrapOneAnother CPPUNIT_TEST_FIXTURE(TestFormula2, testSpilledRangeBindsTighterThanSignAndMarker) { - // The # postfix reads as _xlfn.ANCHORARRAY around the operand it follows, and - // binds tighter than the sign or the @ in front of that operand. + // The # postfix becomes _xlfn.ANCHORARRAY around the operand before it, and binds + // tighter than a sign or an @ in front of that operand. A document from another + // application has the same nesting, so these are the forms actually in use. m_pDoc->InsertTab(0, u"Sheet1"_ustr); + // On its own the wrapper encloses just the reference before the #. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.ANCHORARRAY(A1)"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"A1#"_ustr)); + + // An already parenthesised operand keeps its parentheses inside the wrapper. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.ANCHORARRAY((A1))"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"(A1)#"_ustr)); + // The sign stays outside the wrapper. CPPUNIT_ASSERT_EQUAL(u"-_xlfn.ANCHORARRAY(A1)"_ustr, compileAndPrintAsOoxml(*m_pDoc, u"-A1#"_ustr)); + // A percent sign after the # ends up after both wrappers. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(_xlfn.ANCHORARRAY(A1))%"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@A1#%"_ustr)); + // The @ wrapper goes around the # one, whatever form the operand takes. CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(_xlfn.ANCHORARRAY(A1))"_ustr, compileAndPrintAsOoxml(*m_pDoc, u"@A1#"_ustr)); @@ -7140,6 +7158,16 @@ CPPUNIT_TEST_FIXTURE(TestFormula2, testUnionStaysInsideImplicitIntersectionWrapp CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(_xlfn.ANCHORARRAY((A1,B2)))"_ustr, compileAndPrintAsOoxml(*m_pDoc, u"A1~@B2#"_ustr)); + // An already parenthesised union list fills the wrapper on its own, for either operator. + // Both forms show up in documents from another application. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.ANCHORARRAY((A1,B2))"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"(A1~B2)#"_ustr)); + + // A percent sign closes the wrapper before it, so the union list that opens after it + // starts at the wrapper and the scope's parentheses hold the list. + CPPUNIT_ASSERT_EQUAL(u"(_xlfn.SINGLE( A1)%,B2)"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"(@ A1%~B2)"_ustr)); + m_pDoc->DeleteTab(0); } @@ -7156,9 +7184,16 @@ CPPUNIT_TEST_FIXTURE(TestFormula2, testPostfixOperatorsApplyToTheWholeExpression // The range operator binds tighter still, and a range built with it goes // inside the wrapper whole. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.ANCHORARRAY(A1:A3)"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"A1:A3#"_ustr)); CPPUNIT_ASSERT_EQUAL(u"_xlfn.ANCHORARRAY(A1:INDEX(B1:B10,5))"_ustr, compileAndPrintAsOoxml(*m_pDoc, u"A1:INDEX(B1:B10;5)#"_ustr)); + // A percent sign on one part of a union list keeps that part inside the list, and one + // after the list goes after the list's own parentheses. Neither shape comes out of a + // parse, which treats a percent sign as being above the union operator. + CPPUNIT_ASSERT_EQUAL(u"(A1%,B2)%"_ustr, compileAndPrintAsOoxml(*m_pDoc, u"A1%~B2%"_ustr)); + // A prefix sign on one part of a union list stays outside the wrapper too. CPPUNIT_ASSERT_EQUAL(u"(A1,-_xlfn.ANCHORARRAY(B2))"_ustr, compileAndPrintAsOoxml(*m_pDoc, u"A1~-B2#"_ustr)); @@ -7189,8 +7224,8 @@ CPPUNIT_TEST_FIXTURE(TestFormula2, testUnionParenthesesEncloseExactlyTheirList) CPPUNIT_ASSERT_EQUAL(u"($A$1,_xlfn.SINGLE($B$2))"_ustr, printAsOoxml(*m_pDoc, aMarkerInList, ScAddress(1, 0, 0))); - // A percent sign in front of the union operator, likewise. The sign follows the - // first part of the list, so the list still begins in front of that part. + // Same for a percent sign before the union operator. The sign comes after the first part + // of the list, so the list still starts before that part. ScTokenArray aPercentInList(*m_pDoc); aPercentInList.AddSingleReference(aFirst); aPercentInList.AddOpCode(ocPercentSign); diff --git a/sc/source/filter/oox/formulabuffer.cxx b/sc/source/filter/oox/formulabuffer.cxx index 0b18d6ed3bdd..49002b627d51 100644 --- a/sc/source/filter/oox/formulabuffer.cxx +++ b/sc/source/filter/oox/formulabuffer.cxx @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -78,16 +79,16 @@ void stripRedundantParentheses(ScTokenArray& rArray, } // Rewrite each four-token span -// ocSpill ocOpen ocClose +// ocAnchorArray ocOpen ocClose // to // ocSpill -// so the parse array matches the native postfix form. +// so the parse array matches the native postfix form. Inverse of what the export does. void liftAnchorArrayToPostfix(ScTokenArray& rArray) { sal_uInt16 nPosition = 0; while (nPosition + 1 < rArray.GetLen()) { - if (rArray.TokenAt(nPosition)->GetOpCode() == ocSpill + if (rArray.TokenAt(nPosition)->GetOpCode() == ocAnchorArray && rArray.TokenAt(nPosition + 1)->GetOpCode() == ocOpen) { // Find the parenthesis that closes the one right after the @@ -106,12 +107,9 @@ void liftAnchorArrayToPostfix(ScTokenArray& rArray) } if (nClose < rArray.GetLen()) { - // Turn #(operand) into operand#: move the spill opcode - // past the operand and drop the wrapper parentheses, - // keeping any inner ones. The reference keeps the spill - // token alive when it leaves its old slot. - formula::FormulaTokenRef pSpill(rArray.TokenAt(nPosition)); - rArray.ReplaceToken(nClose, pSpill.get(), + // Put the postfix operator behind the operand and drop the + // wrapper parentheses, keeping any inner ones. + rArray.ReplaceToken(nClose, new formula::FormulaByteToken(ocSpill), formula::FormulaTokenArray::CODE_ONLY); rArray.RemoveToken(nPosition + 1, 1); rArray.RemoveToken(nPosition, 1);