cui/source/dialogs/SpellDialog.cxx | 23 ++++++++++++++++--- i18npool/qa/cppunit/test_breakiterator.cxx | 18 ++++++++++++++ lingucomponent/source/spellcheck/spell/sspellimp.cxx | 23 +++++++++++++++---- linguistic/source/dicimp.cxx | 13 +++++++++- sw/qa/extras/layout/layout2.cxx | 20 ++++++++++++++++ sw/source/uibase/lingu/olmenu.cxx | 18 ++++++++++++++ sw/source/uibase/shells/textsh1.cxx | 21 ++++++++++++++--- 7 files changed, 122 insertions(+), 14 deletions(-)
New commits: commit 54b14742892638e1602bcbe5712a3d48163d4c9e Author: László Németh <[email protected]> AuthorDate: Sun Dec 28 05:04:17 2025 +0100 Commit: Xisco Fauli <[email protected]> CommitDate: Mon Dec 29 17:24:53 2025 +0100 tdf#130695 sw linguistic: fix custom abbreviations linguistic: Abbreviations in the custom dictionary were accepted without the terminating dot, too (because bSimilarOnly comparison used by getEntry() removes all terminating dots during the dictionary look up.) Because "Add to [custom] dictionary" always removed the terminating dot, and direct editing of the custom dictionaries is still not so comfortable, the following user-friendly options were implemented for the abbreviations: – spell checking dialog: words with multiple dots – e.g. F.A.C.S. –, are always added to the custom dictionary with the terminating dot to reject the bad abbreviations: "e.g", "F.A.C.S" etc.; – context menu: add optional menu items to the context menu of the abbreviations to put the word with the terminating dot into the custom dictionaries, e.g. for the word "Corp.": * standard.dic (Corp) * standard.dic (Corp.) as menu items of the the Add to Dictionary submenu. Change-Id: I54532c0a63af8effe9f103e75e853703d7d57563 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/196263 Reviewed-by: László Németh <[email protected]> Tested-by: Jenkins Signed-off-by: Xisco Fauli <[email protected]> Reviewed-on: https://gerrit.libreoffice.org/c/core/+/196284 diff --git a/cui/source/dialogs/SpellDialog.cxx b/cui/source/dialogs/SpellDialog.cxx index 381612ebf2e6..e446e9164694 100644 --- a/cui/source/dialogs/SpellDialog.cxx +++ b/cui/source/dialogs/SpellDialog.cxx @@ -603,6 +603,23 @@ IMPL_LINK_NOARG(SpellDialog, ChangeHdl, weld::Button&, void) m_xIgnorePB->grab_focus(); } +// words with multiple dots are added to the custom +// dictionary with the terminating dot +static bool lcl_IsStripDot( const OUString & rWord ) +{ + // strip the terminating dot, if there is no + // an extra dot inside the word, e.g. + // put "dots." as "dots" into the dictionary, + // but put "F.A.C.S." as "F.A.C.S.", not "F.A.C.S" + if ( rWord.endsWith(".") ) { + sal_Int32 nDotPos = rWord.indexOf("."); + if ( nDotPos == rWord.getLength() - 1 ) + return true; + } + + return false; +} + IMPL_LINK_NOARG(SpellDialog, ChangeAllHdl, weld::Button&, void) { auto xGuard(std::make_unique<UndoChangeGroupGuard>(*m_xSentenceED)); @@ -615,7 +632,7 @@ IMPL_LINK_NOARG(SpellDialog, ChangeAllHdl, weld::Button&, void) Reference<XDictionary> aXDictionary = LinguMgr::GetChangeAllList(); DictionaryError nAdded = AddEntryToDic( aXDictionary, aOldWord, true, - aString ); + aString, lcl_IsStripDot(aOldWord) ); if(nAdded == DictionaryError::NONE) { @@ -661,7 +678,7 @@ IMPL_LINK( SpellDialog, IgnoreAllHdl, weld::Button&, rButton, void ) OUString sErrorText(m_xSentenceED->GetErrorText()); DictionaryError nAdded = AddEntryToDic( aXDictionary, sErrorText, false, - OUString() ); + OUString(), lcl_IsStripDot(sErrorText) ); if (nAdded == DictionaryError::NONE) { std::unique_ptr<SpellUndoAction_Impl> pAction(new SpellUndoAction_Impl( @@ -914,7 +931,7 @@ void SpellDialog::AddToDictionaryExecute(const OUString& rItemId) DictionaryError nAddRes = DictionaryError::UNKNOWN; if (xDic.is()) { - nAddRes = AddEntryToDic( xDic, aNewWord, false, OUString() ); + nAddRes = AddEntryToDic( xDic, aNewWord, false, OUString(), lcl_IsStripDot(aNewWord) ); // save modified user-dictionary if it is persistent uno::Reference< frame::XStorable > xSavDic( xDic, uno::UNO_QUERY ); if (xSavDic.is()) diff --git a/linguistic/source/dicimp.cxx b/linguistic/source/dicimp.cxx index 9c243c794933..24f6384cab3e 100644 --- a/linguistic/source/dicimp.cxx +++ b/linguistic/source/dicimp.cxx @@ -834,8 +834,19 @@ uno::Reference< XDictionaryEntry > SAL_CALL DictionaryNeo::getEntry( if (bNeedEntries) loadEntries( aMainURL ); + // first search: ignore the dots at the end of the + // dictionary words and at the end of the query word sal_Int32 nPos; - bool bFound = seekEntry( aWord, &nPos, true ); + bool bFound = seekEntry( aWord, &nPos,/*bSimilarOnly=*/true ); + // don't accept an abbreviation without the dot, i.e. when the + // searched word is without dot, but the dictionary word is there + // in the dictionary only with dot + if ( bFound && !aWord.endsWith(".") && aEntries[nPos]->getDictionaryWord().endsWith(".") ) + { + sal_Int32 nPos2; + if ( !seekEntry( aWord, &nPos2, /*bSimilarOnly=*/false ) ) + bFound = false; + } DBG_ASSERT(!bFound || nPos < static_cast<sal_Int32>(aEntries.size()), "lng : index out of range"); return bFound ? aEntries[ nPos ] diff --git a/sw/source/uibase/lingu/olmenu.cxx b/sw/source/uibase/lingu/olmenu.cxx index 1bb4dbdd07a3..6ae1056e5289 100644 --- a/sw/source/uibase/lingu/olmenu.cxx +++ b/sw/source/uibase/lingu/olmenu.cxx @@ -324,6 +324,13 @@ SwSpellPopup::SwSpellPopup( m_aDics = xDicList->getDictionaries(); + OUString sWord; + if ( m_xSpellAlt.is() ) + sWord = m_xSpellAlt->getWord(); + // allow to put the word with terminating dot to the custom dictionaries + // using optional menu items + bool bPossibleAbbreviation = sWord.endsWith("."); + for (const uno::Reference<linguistic2::XDictionary>& rDic : m_aDics) { uno::Reference< linguistic2::XDictionary > xDicTmp = rDic; @@ -339,7 +346,9 @@ SwSpellPopup::SwSpellPopup( { // the extra 1 is because of the (possible) external // linguistic entry above - pMenu->InsertItem( nItemId, xDicTmp->getName() ); + pMenu->InsertItem( nItemId, bPossibleAbbreviation + ? xDicTmp->getName() + " (" + sWord.subView(0, sWord.getLength()-1) + ")" + : xDicTmp->getName() ); m_aDicNameSingle = xDicTmp->getName(); if (bUseImagesInMenus) @@ -358,6 +367,13 @@ SwSpellPopup::SwSpellPopup( } ++nItemId; + + // add an optional menu item for adding the word with dot to this dictionary + if ( bPossibleAbbreviation ) + { + pMenu->InsertItem( nItemId, xDicTmp->getName() + " (" + sWord + ")"); + ++nItemId; + } } } } diff --git a/sw/source/uibase/shells/textsh1.cxx b/sw/source/uibase/shells/textsh1.cxx index cd26bb553ead..6388271b990b 100644 --- a/sw/source/uibase/shells/textsh1.cxx +++ b/sw/source/uibase/shells/textsh1.cxx @@ -927,7 +927,7 @@ bool lcl_DeleteChartColumns(const uno::Reference<chart2::XChartDocument>& xChart } } -static bool AddWordToWordbook(const uno::Reference<linguistic2::XDictionary>& xDictionary, SwWrtShell &rWrtSh) +static bool AddWordToWordbook(const uno::Reference<linguistic2::XDictionary>& xDictionary, SwWrtShell &rWrtSh, bool bAddWithDot = false) { if (!xDictionary) return false; @@ -938,7 +938,7 @@ static bool AddWordToWordbook(const uno::Reference<linguistic2::XDictionary>& xD return false; OUString sWord = xSpellAlt->getWord(); - linguistic::DictionaryError nAddRes = linguistic::AddEntryToDic(xDictionary, sWord, false, OUString()); + linguistic::DictionaryError nAddRes = linguistic::AddEntryToDic(xDictionary, sWord, false, OUString(), !bAddWithDot); if (linguistic::DictionaryError::NONE != nAddRes && xDictionary.is() && !xDictionary->getEntry(sWord).is()) { SvxDicError(rWrtSh.GetView().GetFrameWeld(), nAddRes); @@ -2377,7 +2377,7 @@ void SwTextShell::Execute(SfxRequest &rReq) } else if (sApplyText == "Spelling") { - AddWordToWordbook(LinguMgr::GetIgnoreAllList(), rWrtSh); + AddWordToWordbook(LinguMgr::GetIgnoreAllList(), rWrtSh ); } } break; @@ -2387,9 +2387,22 @@ void SwTextShell::Execute(SfxRequest &rReq) if (const SfxStringItem* pItem1 = rReq.GetArg<SfxStringItem>(FN_PARAM_1)) aDicName = pItem1->GetValue(); + // strip dot, if the extended dictionary name ends with ")", but not ".)", e.g. + // "standard.dic (F.A.C.S)" -> strip dot + // "standard.dic (F.A.C.S.)" -> with dot + bool bAddWithDot = false; + if ( aDicName.endsWith(")") ) + { + bAddWithDot = aDicName.endsWith(".)"); + // restore the dictionary name by stripping the parenthesized extension + sal_Int32 nHintPos = aDicName.indexOf(" ("); + if ( nHintPos > 0 ) + aDicName = aDicName.copy(0, nHintPos); + } + uno::Reference<linguistic2::XSearchableDictionaryList> xDicList(LinguMgr::GetDictionaryList()); uno::Reference<linguistic2::XDictionary> xDic = xDicList.is() ? xDicList->getDictionaryByName(aDicName) : nullptr; - if (AddWordToWordbook(xDic, rWrtSh)) + if (AddWordToWordbook(xDic, rWrtSh, bAddWithDot)) { // save modified user-dictionary if it is persistent uno::Reference<frame::XStorable> xSavDic(xDic, uno::UNO_QUERY); commit 5fad461dd7d85e0fe41de621e49933b251a629ce Author: László Németh <[email protected]> AuthorDate: Sat Dec 27 13:40:51 2025 +0100 Commit: Xisco Fauli <[email protected]> CommitDate: Mon Dec 29 17:24:42 2025 +0100 tdf#170140 lingucomponent: check words with non-ASCII apostrophe If the spelling dictionary contains them with non-ASCII apostrophe. Previously the words were checked only with apostrophes converted to their ASCII version, resulting continuous false alarms, despite the correct orthography in the document and in the spelling dictionary. Now the words are checked both with ASCII conversion and with the original non-ASCII apostrophes (because still there are UTF-8 dictionaries with ASCII apostrophes, moreover, dictionaries containing mixed apostrophes in the same dic file). Example words for the Hungarian dictionary: d’Arc, d’Alembert, McDonald’s (and their several recognized suffixed forms: d’Arcért, d’Arckal, d’Arcként, d’Arcnak, d’Arcot etc. etc.) Add unit tests for 1) break iterator and 2) spell checking with non-ASCII apostrophes. Change-Id: I24a570df41fa5aba2e7b67dde0db33377717dc2a Reviewed-on: https://gerrit.libreoffice.org/c/core/+/196248 Reviewed-by: László Németh <[email protected]> Tested-by: Jenkins Signed-off-by: Xisco Fauli <[email protected]> Reviewed-on: https://gerrit.libreoffice.org/c/core/+/196283 diff --git a/i18npool/qa/cppunit/test_breakiterator.cxx b/i18npool/qa/cppunit/test_breakiterator.cxx index ae2e5af4e5e9..c91532d5ff3a 100644 --- a/i18npool/qa/cppunit/test_breakiterator.cxx +++ b/i18npool/qa/cppunit/test_breakiterator.cxx @@ -1844,7 +1844,18 @@ void TestBreakIterator::testDictWordAbbreviationHU() for (const auto& rLocale : aLocale) { - auto aTest = u"Pl. stb. dr.-ral Mo.-gal 50-et 50.-et"_ustr; + + auto aTest = + // abbreviations + u"Pl. stb. " + // abbreviations with suffixes + "dr.-ral Mo.-gal " + // number with a suffix + "50-et " + // ordinal number with a suffix + "50.-et " + // word with a non-ASCII apostrophe + "d’Arc"_ustr; i18n::Boundary aBounds = m_xBreak->getWordBoundary(aTest, 1, rLocale, i18n::WordType::DICTIONARY_WORD, false); @@ -1875,6 +1886,11 @@ void TestBreakIterator::testDictWordAbbreviationHU() = m_xBreak->getWordBoundary(aTest, 31, rLocale, i18n::WordType::DICTIONARY_WORD, false); CPPUNIT_ASSERT_EQUAL(sal_Int32(31), aBounds.startPos); CPPUNIT_ASSERT_EQUAL(sal_Int32(37), aBounds.endPos); + + aBounds + = m_xBreak->getWordBoundary(aTest, 38, rLocale, i18n::WordType::DICTIONARY_WORD, false); + CPPUNIT_ASSERT_EQUAL(sal_Int32(38), aBounds.startPos); + CPPUNIT_ASSERT_EQUAL(sal_Int32(43), aBounds.endPos); } } diff --git a/lingucomponent/source/spellcheck/spell/sspellimp.cxx b/lingucomponent/source/spellcheck/spell/sspellimp.cxx index fe676cde5312..153ce18ec2be 100644 --- a/lingucomponent/source/spellcheck/spell/sspellimp.cxx +++ b/lingucomponent/source/spellcheck/spell/sspellimp.cxx @@ -243,6 +243,7 @@ sal_Bool SAL_CALL SpellChecker::hasLocale(const Locale& rLocale) return bRes; } +#define SPELL_NON_ASCII_APOSTROPHE 1 << 10 sal_Int16 SpellChecker::GetSpellFailure(const OUString &rWord, const Locale &rLocale, int& rInfo) { if (rWord.getLength() > MAXWORDLEN) @@ -261,15 +262,19 @@ sal_Int16 SpellChecker::GetSpellFailure(const OUString &rWord, const Locale &rLo sal_Int32 n = rBuf.getLength(); sal_Unicode c; sal_Int32 extrachar = 0; - + const bool bDoNotConvertApostrophe = bool(rInfo & SPELL_NON_ASCII_APOSTROPHE); + bool bHasNonASCIIApostrophe = false; + rInfo = 0; for (sal_Int32 ix=0; ix < n; ix++) { c = rBuf[ix]; if ((c == 0x201C) || (c == 0x201D)) rBuf[ix] = u'"'; - else if ((c == 0x2018) || (c == 0x2019)) + else if (!bDoNotConvertApostrophe && ((c == 0x2018) || (c == 0x2019))) + { rBuf[ix] = u'\''; - + bHasNonASCIIApostrophe = true; + } // recognize words with Unicode ligatures and ZWNJ/ZWJ characters (only // with 8-bit encoded dictionaries. For UTF-8 encoded dictionaries // set ICONV and IGNORE aff file options, if needed.) @@ -370,6 +375,10 @@ sal_Int16 SpellChecker::GetSpellFailure(const OUString &rWord, const Locale &rLo } } + // checked with apostrophe conversion + if ( bHasNonASCIIApostrophe ) + rInfo |= SPELL_NON_ASCII_APOSTROPHE; + return nRes; } @@ -396,8 +405,14 @@ sal_Bool SAL_CALL SpellChecker::isValid( const OUString& rWord, const Locale& rL PropertyHelper_Spelling& rHelper = GetPropHelper(); rHelper.SetTmpPropVals( rProperties ); - int nInfo = 0; + int nInfo = 0; // return compound information, disable apostrophe conversion sal_Int16 nFailure = GetSpellFailure( rWord, rLocale, nInfo ); + // it contains non-ASCII apostrophe, and it was bad with ASCII conversion: + // check the word with the original apostrophe character(s), too + if ( nFailure != -1 && nInfo & SPELL_NON_ASCII_APOSTROPHE ) { + nInfo = SPELL_NON_ASCII_APOSTROPHE; // disable apostrophe conversion + nFailure = GetSpellFailure( rWord, rLocale, nInfo ); + } if (nFailure != -1 && !rWord.match(SPELL_XML, 0)) { LanguageType nLang = LinguLocaleToLanguage( rLocale ); diff --git a/sw/qa/extras/layout/layout2.cxx b/sw/qa/extras/layout/layout2.cxx index f332a5226806..b288a036359d 100644 --- a/sw/qa/extras/layout/layout2.cxx +++ b/sw/qa/extras/layout/layout2.cxx @@ -1275,6 +1275,26 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter2, testTdf158885_not_compound_remain) u"lenes emberellenes emberellenes emberellenes emberellenes emberellenes "); } +// TODO: move this test to the lingucomponent project +CPPUNIT_TEST_FIXTURE(SwLayoutWriter2, testTdf170140) +{ + uno::Reference<linguistic2::XSpellChecker1> xSpell = LinguMgr::GetSpellChecker(); + auto aLocale = lang::Locale(u"hu"_ustr, u"HU"_ustr, OUString()); + LanguageType eLang = LanguageTag::convertToLanguageType(aLocale); + if (!xSpell.is() || !xSpell->hasLanguage(static_cast<sal_uInt16>(eLang))) + return; + + uno::Sequence<beans::PropertyValue> aProperties; + + // correct non-ASCII apostrophe + OUString sWord(u"d’Arc"_ustr); + CPPUNIT_ASSERT(xSpell->isValid(sWord, static_cast<sal_uInt16>(eLang), aProperties)); + + // bad ASCII apostrophe + OUString sWord2(u"d'Arc"_ustr); + CPPUNIT_ASSERT(!xSpell->isValid(sWord2, static_cast<sal_uInt16>(eLang), aProperties)); +} + CPPUNIT_TEST_FIXTURE(SwLayoutWriter2, testRedlineNumberInFootnote) { createSwDoc("tdf85610.fodt");
