From ed5640d8b587fbcfed7dd7967f3de04b37a76f26 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 11:06:44 +0200 Subject: Adding upstream version 4:7.4.7. Signed-off-by: Daniel Baumann --- .../spellcheck/languagetool/LanguageTool.component | 26 + .../spellcheck/languagetool/languagetoolimp.cxx | 407 +++++++++++++ .../spellcheck/languagetool/languagetoolimp.hxx | 91 +++ .../spellcheck/macosxspell/MacOSXSpell.component | 26 + .../source/spellcheck/macosxspell/macspellimp.hxx | 123 ++++ .../source/spellcheck/macosxspell/macspellimp.mm | 666 +++++++++++++++++++++ .../source/spellcheck/spell/spell.component | 26 + .../source/spellcheck/spell/sspellimp.cxx | 635 ++++++++++++++++++++ .../source/spellcheck/spell/sspellimp.hxx | 120 ++++ 9 files changed, 2120 insertions(+) create mode 100644 lingucomponent/source/spellcheck/languagetool/LanguageTool.component create mode 100644 lingucomponent/source/spellcheck/languagetool/languagetoolimp.cxx create mode 100644 lingucomponent/source/spellcheck/languagetool/languagetoolimp.hxx create mode 100644 lingucomponent/source/spellcheck/macosxspell/MacOSXSpell.component create mode 100644 lingucomponent/source/spellcheck/macosxspell/macspellimp.hxx create mode 100644 lingucomponent/source/spellcheck/macosxspell/macspellimp.mm create mode 100644 lingucomponent/source/spellcheck/spell/spell.component create mode 100644 lingucomponent/source/spellcheck/spell/sspellimp.cxx create mode 100644 lingucomponent/source/spellcheck/spell/sspellimp.hxx (limited to 'lingucomponent/source/spellcheck') diff --git a/lingucomponent/source/spellcheck/languagetool/LanguageTool.component b/lingucomponent/source/spellcheck/languagetool/LanguageTool.component new file mode 100644 index 000000000..9f7eb3d08 --- /dev/null +++ b/lingucomponent/source/spellcheck/languagetool/LanguageTool.component @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/lingucomponent/source/spellcheck/languagetool/languagetoolimp.cxx b/lingucomponent/source/spellcheck/languagetool/languagetoolimp.cxx new file mode 100644 index 000000000..06b4fcb64 --- /dev/null +++ b/lingucomponent/source/spellcheck/languagetool/languagetoolimp.cxx @@ -0,0 +1,407 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include "languagetoolimp.hxx" + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace osl; +using namespace com::sun::star; +using namespace com::sun::star::beans; +using namespace com::sun::star::lang; +using namespace com::sun::star::uno; +using namespace com::sun::star::linguistic2; +using namespace linguistic; + +#define COL_ORANGE Color(0xD1, 0x68, 0x20) + +namespace +{ +PropertyValue lcl_MakePropertyValue(const OUString& rName, uno::Any& rValue) +{ + return PropertyValue(rName, -1, rValue, css::beans::PropertyState_DIRECT_VALUE); +} + +Sequence lcl_GetLineColorPropertyFromErrorId(const std::string& rErrorId) +{ + uno::Any aColor; + if (rErrorId == "TYPOS") + { + aColor <<= COL_LIGHTRED; + } + else if (rErrorId == "STYLE") + { + aColor <<= COL_LIGHTBLUE; + } + else + { + // Same color is used for other errorId's such as GRAMMAR, TYPOGRAPHY.. + aColor <<= COL_ORANGE; + } + Sequence aProperties{ lcl_MakePropertyValue("LineColor", aColor) }; + return aProperties; +} +} + +LanguageToolGrammarChecker::LanguageToolGrammarChecker() + : mCachedResults(MAX_CACHE_SIZE) +{ +} + +LanguageToolGrammarChecker::~LanguageToolGrammarChecker() {} + +sal_Bool SAL_CALL LanguageToolGrammarChecker::isSpellChecker() { return false; } + +sal_Bool SAL_CALL LanguageToolGrammarChecker::hasLocale(const Locale& rLocale) +{ + bool bRes = false; + if (!m_aSuppLocales.hasElements()) + getLocales(); + + for (auto const& suppLocale : std::as_const(m_aSuppLocales)) + { + if (rLocale == suppLocale) + { + bRes = true; + break; + } + } + + return bRes; +} + +Sequence SAL_CALL LanguageToolGrammarChecker::getLocales() +{ + if (m_aSuppLocales.hasElements()) + return m_aSuppLocales; + SvxLanguageToolOptions& rLanguageOpts = SvxLanguageToolOptions::Get(); + OString localeUrl = OUStringToOString(rLanguageOpts.getLocaleListURL(), RTL_TEXTENCODING_UTF8); + if (localeUrl.isEmpty()) + { + return m_aSuppLocales; + } + tools::Long statusCode = 0; + std::string response = makeHttpRequest(localeUrl, HTTP_METHOD::HTTP_GET, OString(), statusCode); + if (statusCode != 200) + { + return m_aSuppLocales; + } + if (response.empty()) + { + return m_aSuppLocales; + } + boost::property_tree::ptree root; + std::stringstream aStream(response); + boost::property_tree::read_json(aStream, root); + + size_t length = root.size(); + m_aSuppLocales.realloc(length); + auto pArray = m_aSuppLocales.getArray(); + int i = 0; + for (auto it = root.begin(); it != root.end(); it++, i++) + { + boost::property_tree::ptree& localeItem = it->second; + const std::string longCode = localeItem.get("longCode"); + Locale aLocale = LanguageTag::convertToLocale( + OUString(longCode.c_str(), longCode.length(), RTL_TEXTENCODING_UTF8)); + pArray[i] = aLocale; + } + return m_aSuppLocales; +} + +// Callback to get the response data from server. +static size_t WriteCallback(void* ptr, size_t size, size_t nmemb, void* userp) +{ + if (!userp) + return 0; + + std::string* response = static_cast(userp); + size_t real_size = size * nmemb; + response->append(static_cast(ptr), real_size); + return real_size; +} + +ProofreadingResult SAL_CALL LanguageToolGrammarChecker::doProofreading( + const OUString& aDocumentIdentifier, const OUString& aText, const Locale& aLocale, + sal_Int32 nStartOfSentencePosition, sal_Int32 nSuggestedBehindEndOfSentencePosition, + const Sequence& aProperties) +{ + // ProofreadingResult declared here instead of parseHttpJSONResponse because of the early exists. + ProofreadingResult xRes; + xRes.aDocumentIdentifier = aDocumentIdentifier; + xRes.aText = aText; + xRes.aLocale = aLocale; + xRes.nStartOfSentencePosition = nStartOfSentencePosition; + xRes.nBehindEndOfSentencePosition = nSuggestedBehindEndOfSentencePosition; + xRes.aProperties = Sequence(); + xRes.xProofreader = this; + xRes.aErrors = Sequence(); + + if (aText.isEmpty()) + { + return xRes; + } + + if (nStartOfSentencePosition != 0) + { + return xRes; + } + + xRes.nStartOfNextSentencePosition = aText.getLength(); + + SvxLanguageToolOptions& rLanguageOpts = SvxLanguageToolOptions::Get(); + if (rLanguageOpts.getEnabled() == false) + { + return xRes; + } + + OString checkerURL = OUStringToOString(rLanguageOpts.getCheckerURL(), RTL_TEXTENCODING_UTF8); + if (checkerURL.isEmpty()) + { + return xRes; + } + + if (aProperties.getLength() > 0 && aProperties[0].Name == "Update") + { + // locale changed + xRes.aText = ""; + return xRes; + } + + sal_Int32 spaceIndex = std::min(xRes.nStartOfNextSentencePosition, aText.getLength() - 1); + while (spaceIndex < aText.getLength() && aText[spaceIndex] == ' ') + { + xRes.nStartOfNextSentencePosition += 1; + spaceIndex = xRes.nStartOfNextSentencePosition; + } + if (xRes.nStartOfNextSentencePosition == nSuggestedBehindEndOfSentencePosition + && spaceIndex < aText.getLength()) + { + xRes.nStartOfNextSentencePosition + = std::min(nSuggestedBehindEndOfSentencePosition + 1, aText.getLength()); + } + xRes.nBehindEndOfSentencePosition + = std::min(xRes.nStartOfNextSentencePosition, aText.getLength()); + + auto cachedResult = mCachedResults.find(aText); + if (cachedResult != mCachedResults.end()) + { + xRes.aErrors = cachedResult->second; + return xRes; + } + + tools::Long http_code = 0; + OUString langTag(aLocale.Language + "-" + aLocale.Country); + OString postData(OUStringToOString( + OUStringConcatenation("text=" + aText + "&language=" + langTag), RTL_TEXTENCODING_UTF8)); + const std::string response_body + = makeHttpRequest(checkerURL, HTTP_METHOD::HTTP_POST, postData, http_code); + + if (http_code != 200) + { + return xRes; + } + + if (response_body.length() <= 0) + { + return xRes; + } + + parseProofreadingJSONResponse(xRes, response_body); + // cache the result + mCachedResults.insert( + std::pair>(aText, xRes.aErrors)); + return xRes; +} + +/* + rResult is both input and output + aJSONBody is the response body from the HTTP Request to LanguageTool API +*/ +void LanguageToolGrammarChecker::parseProofreadingJSONResponse(ProofreadingResult& rResult, + std::string_view aJSONBody) +{ + boost::property_tree::ptree root; + std::stringstream aStream(aJSONBody.data()); + boost::property_tree::read_json(aStream, root); + boost::property_tree::ptree& matches = root.get_child("matches"); + size_t matchSize = matches.size(); + + if (matchSize <= 0) + { + return; + } + Sequence aErrors(matchSize); + auto pErrors = aErrors.getArray(); + size_t i = 0; + for (auto it1 = matches.begin(); it1 != matches.end(); it1++, i++) + { + const boost::property_tree::ptree& match = it1->second; + int offset = match.get("offset"); + int length = match.get("length"); + const std::string shortMessage = match.get("message"); + const std::string message = match.get("shortMessage"); + + // Parse the error category for Line Color + const boost::property_tree::ptree& rule = match.get_child("rule"); + const boost::property_tree::ptree& ruleCategory = rule.get_child("category"); + const std::string errorCategoryId = ruleCategory.get("id"); + + OUString aShortComment(shortMessage.c_str(), shortMessage.length(), RTL_TEXTENCODING_UTF8); + OUString aFullComment(message.c_str(), message.length(), RTL_TEXTENCODING_UTF8); + + pErrors[i].nErrorStart = offset; + pErrors[i].nErrorLength = length; + pErrors[i].nErrorType = PROOFREADING_ERROR; + pErrors[i].aShortComment = aShortComment; + pErrors[i].aFullComment = aFullComment; + pErrors[i].aProperties = lcl_GetLineColorPropertyFromErrorId(errorCategoryId); + ; + const boost::property_tree::ptree& replacements = match.get_child("replacements"); + int suggestionSize = replacements.size(); + + if (suggestionSize <= 0) + { + continue; + } + pErrors[i].aSuggestions.realloc(std::min(suggestionSize, MAX_SUGGESTIONS_SIZE)); + auto pSuggestions = pErrors[i].aSuggestions.getArray(); + // Limit suggestions to avoid crash on context menu popup: + // (soffice:17251): Gdk-CRITICAL **: 17:00:21.277: ../../../../../gdk/wayland/gdkdisplay-wayland.c:1399: Unable to create Cairo image + // surface: invalid value (typically too big) for the size of the input (surface, pattern, etc.) + int j = 0; + for (auto it2 = replacements.begin(); it2 != replacements.end() && j < MAX_SUGGESTIONS_SIZE; + it2++, j++) + { + const boost::property_tree::ptree& replacement = it2->second; + std::string replacementStr = replacement.get("value"); + pSuggestions[j] + = OUString(replacementStr.c_str(), replacementStr.length(), RTL_TEXTENCODING_UTF8); + } + } + rResult.aErrors = aErrors; +} + +std::string LanguageToolGrammarChecker::makeHttpRequest(std::string_view aURL, HTTP_METHOD method, + const OString& aPostData, + tools::Long& nStatusCode) +{ + std::unique_ptr> curl(curl_easy_init(), + [](CURL* p) { curl_easy_cleanup(p); }); + if (!curl) + return {}; // empty string + + bool isPremium = false; + SvxLanguageToolOptions& rLanguageOpts = SvxLanguageToolOptions::Get(); + OString apiKey = OUStringToOString(rLanguageOpts.getApiKey(), RTL_TEXTENCODING_UTF8); + OString username = OUStringToOString(rLanguageOpts.getUsername(), RTL_TEXTENCODING_UTF8); + OString premiumPostData; + if (!apiKey.isEmpty() && !username.isEmpty()) + { + isPremium = true; + } + + std::string response_body; + curl_easy_setopt(curl.get(), CURLOPT_URL, aURL.data()); + + curl_easy_setopt(curl.get(), CURLOPT_FAILONERROR, 1L); + // curl_easy_setopt(curl.get(), CURLOPT_VERBOSE, 1L); + + curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, static_cast(&response_body)); + curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, false); + curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, false); + curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, CURL_TIMEOUT); + + if (method == HTTP_METHOD::HTTP_POST) + { + curl_easy_setopt(curl.get(), CURLOPT_POST, 1L); + if (isPremium == false) + { + curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, aPostData.getStr()); + } + else + { + premiumPostData = aPostData + "&username=" + username + "&apiKey=" + apiKey; + curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, premiumPostData.getStr()); + } + } + + /*CURLcode cc = */ + curl_easy_perform(curl.get()); + curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &nStatusCode); + return response_body; +} + +void SAL_CALL LanguageToolGrammarChecker::ignoreRule(const OUString& /*aRuleIdentifier*/, + const Locale& /*aLocale*/ +) +{ +} +void SAL_CALL LanguageToolGrammarChecker::resetIgnoreRules() {} + +OUString SAL_CALL LanguageToolGrammarChecker::getServiceDisplayName(const Locale& rLocale) +{ + std::locale loc(Translate::Create("svt", LanguageTag(rLocale))); + return Translate::get(STR_DESCRIPTION_LANGUAGETOOL, loc); +} + +OUString SAL_CALL LanguageToolGrammarChecker::getImplementationName() +{ + return "org.openoffice.lingu.LanguageToolGrammarChecker"; +} + +sal_Bool SAL_CALL LanguageToolGrammarChecker::supportsService(const OUString& ServiceName) +{ + return cppu::supportsService(this, ServiceName); +} + +Sequence SAL_CALL LanguageToolGrammarChecker::getSupportedServiceNames() +{ + return { SN_GRAMMARCHECKER }; +} + +void SAL_CALL LanguageToolGrammarChecker::initialize(const Sequence& /*rArguments*/) {} + +extern "C" SAL_DLLPUBLIC_EXPORT css::uno::XInterface* +lingucomponent_LanguageToolGrammarChecker_get_implementation( + css::uno::XComponentContext*, css::uno::Sequence const&) +{ + return cppu::acquire(static_cast(new LanguageToolGrammarChecker())); +} \ No newline at end of file diff --git a/lingucomponent/source/spellcheck/languagetool/languagetoolimp.hxx b/lingucomponent/source/spellcheck/languagetool/languagetoolimp.hxx new file mode 100644 index 000000000..e0dadfeca --- /dev/null +++ b/lingucomponent/source/spellcheck/languagetool/languagetoolimp.hxx @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::beans; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::linguistic2; + +// Magical numbers +#define MAX_CACHE_SIZE 10 +#define MAX_SUGGESTIONS_SIZE 10 +#define PROOFREADING_ERROR 2 +#define CURL_TIMEOUT 10L + +enum class HTTP_METHOD +{ + HTTP_GET, + HTTP_POST +}; + +class LanguageToolGrammarChecker + : public cppu::WeakImplHelper +{ + Sequence m_aSuppLocales; + o3tl::lru_map> mCachedResults; + LanguageToolGrammarChecker(const LanguageToolGrammarChecker&) = delete; + LanguageToolGrammarChecker& operator=(const LanguageToolGrammarChecker&) = delete; + static void parseProofreadingJSONResponse(ProofreadingResult& rResult, + std::string_view aJSONBody); + static std::string makeHttpRequest(std::string_view aURL, HTTP_METHOD method, + const OString& aPostData, tools::Long& nStatusCode); + +public: + LanguageToolGrammarChecker(); + virtual ~LanguageToolGrammarChecker() override; + + // XSupportedLocales + virtual Sequence SAL_CALL getLocales() override; + virtual sal_Bool SAL_CALL hasLocale(const Locale& rLocale) override; + + // XProofReader + virtual sal_Bool SAL_CALL isSpellChecker() override; + virtual ProofreadingResult SAL_CALL doProofreading( + const OUString& aDocumentIdentifier, const OUString& aText, const Locale& aLocale, + sal_Int32 nStartOfSentencePosition, sal_Int32 nSuggestedBehindEndOfSentencePosition, + const Sequence& aProperties) override; + + virtual void SAL_CALL ignoreRule(const OUString& aRuleIdentifier, + const Locale& aLocale) override; + virtual void SAL_CALL resetIgnoreRules() override; + + // XServiceDisplayName + virtual OUString SAL_CALL getServiceDisplayName(const Locale& rLocale) override; + + // XInitialization + virtual void SAL_CALL initialize(const Sequence& rArguments) override; + + // XServiceInfo + virtual OUString SAL_CALL getImplementationName() override; + virtual sal_Bool SAL_CALL supportsService(const OUString& rServiceName) override; + virtual Sequence SAL_CALL getSupportedServiceNames() override; +}; \ No newline at end of file diff --git a/lingucomponent/source/spellcheck/macosxspell/MacOSXSpell.component b/lingucomponent/source/spellcheck/macosxspell/MacOSXSpell.component new file mode 100644 index 000000000..b1fe7d612 --- /dev/null +++ b/lingucomponent/source/spellcheck/macosxspell/MacOSXSpell.component @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/lingucomponent/source/spellcheck/macosxspell/macspellimp.hxx b/lingucomponent/source/spellcheck/macosxspell/macspellimp.hxx new file mode 100644 index 000000000..776c474d2 --- /dev/null +++ b/lingucomponent/source/spellcheck/macosxspell/macspellimp.hxx @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_LINGUCOMPONENT_SOURCE_SPELLCHECK_MACOSXSPELL_MACSPELLIMP_HXX +#define INCLUDED_LINGUCOMPONENT_SOURCE_SPELLCHECK_MACOSXSPELL_MACSPELLIMP_HXX + +#include +#include + +#include +#ifdef MACOSX +#import +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::beans; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::linguistic2; + +class MacSpellChecker : + public cppu::WeakImplHelper + < + XSpellChecker, + XLinguServiceEventBroadcaster, + XInitialization, + XComponent, + XServiceInfo, + XServiceDisplayName + > +{ + Sequence< Locale > aSuppLocales; + rtl_TextEncoding * aDEncs; + Locale * aDLocs; + OUString * aDNames; + sal_Int32 numdict; +#ifdef MACOSX + int macTag; // unique tag for this doc +#else + UITextChecker * pChecker; +#endif + ::comphelper::OInterfaceContainerHelper3 aEvtListeners; + rtl::Reference< linguistic::PropertyHelper_Spell > xPropHelper; + bool bDisposing; + + MacSpellChecker(const MacSpellChecker &) = delete; + MacSpellChecker & operator = (const MacSpellChecker &) = delete; + + linguistic::PropertyHelper_Spell & GetPropHelper_Impl(); + linguistic::PropertyHelper_Spell & GetPropHelper() + { + return xPropHelper.is() ? *xPropHelper : GetPropHelper_Impl(); + } + + sal_Int16 GetSpellFailure( const OUString &rWord, const Locale &rLocale ); + Reference< XSpellAlternatives > GetProposals( const OUString &rWord, const Locale &rLocale ); + +public: + MacSpellChecker(); + virtual ~MacSpellChecker() override; + + // XSupportedLocales (for XSpellChecker) + virtual Sequence< Locale > SAL_CALL getLocales() override; + virtual sal_Bool SAL_CALL hasLocale( const Locale& rLocale ) override; + + // XSpellChecker + virtual sal_Bool SAL_CALL isValid( const OUString& rWord, const Locale& rLocale, const css::uno::Sequence& rProperties ) override; + virtual Reference< XSpellAlternatives > SAL_CALL spell( const OUString& rWord, const Locale& rLocale, const css::uno::Sequence& rProperties ) override; + + // XLinguServiceEventBroadcaster + virtual sal_Bool SAL_CALL addLinguServiceEventListener( const Reference< XLinguServiceEventListener >& rxLstnr ) override; + virtual sal_Bool SAL_CALL removeLinguServiceEventListener( const Reference< XLinguServiceEventListener >& rxLstnr ) override; + + // XServiceDisplayName + virtual OUString SAL_CALL getServiceDisplayName( const Locale& rLocale ) override; + + // XInitialization + virtual void SAL_CALL initialize( const Sequence< Any >& rArguments ) override; + + // XComponent + virtual void SAL_CALL dispose() override; + virtual void SAL_CALL addEventListener( const Reference< XEventListener >& rxListener ) override; + virtual void SAL_CALL removeEventListener( const Reference< XEventListener >& rxListener ) override; + + // XServiceInfo + virtual OUString SAL_CALL getImplementationName() override; + virtual sal_Bool SAL_CALL supportsService( const OUString& rServiceName ) override; + virtual Sequence< OUString > SAL_CALL getSupportedServiceNames() override; +}; + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/lingucomponent/source/spellcheck/macosxspell/macspellimp.mm b/lingucomponent/source/spellcheck/macosxspell/macspellimp.mm new file mode 100644 index 000000000..c20871b1e --- /dev/null +++ b/lingucomponent/source/spellcheck/macosxspell/macspellimp.mm @@ -0,0 +1,666 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "macspellimp.hxx" + +#include +#include +#include +#include +#include +#include + +using namespace utl; +using namespace osl; +using namespace com::sun::star; +using namespace com::sun::star::beans; +using namespace com::sun::star::lang; +using namespace com::sun::star::uno; +using namespace com::sun::star::linguistic2; +using namespace linguistic; + +MacSpellChecker::MacSpellChecker() : + aEvtListeners( GetLinguMutex() ) +{ + aDEncs = nullptr; + aDLocs = nullptr; + aDNames = nullptr; + bDisposing = false; + numdict = 0; +#ifndef IOS + NSApplicationLoad(); + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + macTag = [NSSpellChecker uniqueSpellDocumentTag]; + [pool release]; +#else + pChecker = [[UITextChecker alloc] init]; +#endif +} + + +MacSpellChecker::~MacSpellChecker() +{ + numdict = 0; + if (aDEncs) delete[] aDEncs; + aDEncs = nullptr; + if (aDLocs) delete[] aDLocs; + aDLocs = nullptr; + if (aDNames) delete[] aDNames; + aDNames = nullptr; + if (xPropHelper.is()) + xPropHelper->RemoveAsPropListener(); +} + + +PropertyHelper_Spell & MacSpellChecker::GetPropHelper_Impl() +{ + if (!xPropHelper.is()) + { + Reference< XLinguProperties > xPropSet( GetLinguProperties() ); + + xPropHelper = new PropertyHelper_Spell( static_cast(this), xPropSet ); + xPropHelper->AddAsPropListener(); + } + return *xPropHelper; +} + + +Sequence< Locale > SAL_CALL MacSpellChecker::getLocales() +{ + MutexGuard aGuard( GetLinguMutex() ); + + // this routine should return the locales supported by the installed + // dictionaries. So here we need to parse both the user edited + // dictionary list and the shared dictionary list + // to see what dictionaries the admin/user has installed + + int numshr; // number of shared dictionary entries + rtl_TextEncoding aEnc = RTL_TEXTENCODING_UTF8; + + std::vector postspdict; + + if (!numdict) { + + // invoke a dictionary manager to get the user dictionary list + // TODO How on macOS? + + // invoke a second dictionary manager to get the shared dictionary list +#ifdef MACOSX + NSArray *aSpellCheckLanguages = [[NSSpellChecker sharedSpellChecker] availableLanguages]; +#else + NSArray *aSpellCheckLanguages = [UITextChecker availableLanguages]; +#endif + + for (NSUInteger i = 0; i < [aSpellCheckLanguages count]; i++) + { + NSString* pLangStr = static_cast([aSpellCheckLanguages objectAtIndex:i]); + + // Fix up generic languages (without territory code) and odd combinations that LO + // doesn't handle. + if ([pLangStr isEqualToString:@"da"]) + { + postspdict.push_back( @"da_DK" ); + } + else if ([pLangStr isEqualToString:@"de"]) + { + // Not de_CH and de_LI, though. They need separate dictionaries. + const std::vector aDE + { @"AT", @"BE", @"DE", @"LU" }; + for (auto c: aDE) + { + pLangStr = [@"de_" stringByAppendingString: c]; + postspdict.push_back( pLangStr ); + } + } +#ifdef IOS + // iOS says it has specifically de_DE. Let's assume it is good enough for German as + // written in Austria, Belgium, and Luxembourg, too. (Not for German in Switzerland and + // Liechtenstein. For those you need to bundle the myspell dictionary.) + else if ([pLangStr isEqualToString:@"de_DE"]) + { + const std::vector aDE + { @"AT", @"BE", @"DE", @"LU" }; + for (auto c: aDE) + { + pLangStr = [@"de_" stringByAppendingString: c]; + postspdict.push_back( pLangStr ); + } + } +#endif + else if ([pLangStr isEqualToString:@"en"]) + { + // System has en_AU, en_CA, en_GB, and en_IN. Add the rest. + const std::vector aEN + { @"BW", @"BZ", @"GH", @"GM", @"IE", @"JM", @"MU", @"MW", @"MY", @"NA", + @"NZ", @"PH", @"TT", @"US", @"ZA", @"ZW" }; + for (auto c: aEN) + { + pLangStr = [@"en_" stringByAppendingString: c]; + postspdict.push_back( pLangStr ); + } + } + else if ([pLangStr isEqualToString:@"en_JP"] + || [pLangStr isEqualToString:@"en_SG"]) + { + // Just skip, LO doesn't have those yet in this context. + } + else if ([pLangStr isEqualToString:@"es"]) + { + const std::vector aES + { @"AR", @"BO", @"CL", @"CO", @"CR", @"CU", @"DO", @"EC", @"ES", @"GT", + @"HN", @"MX", @"NI", @"PA", @"PE", @"PR", @"PY", @"SV", @"UY", @"VE" }; + for (auto c: aES) + { + pLangStr = [@"es_" stringByAppendingString: c]; + postspdict.push_back( pLangStr ); + } + } + else if ([pLangStr isEqualToString:@"fi"]) + { + postspdict.push_back( @"fi_FI" ); + } + else if ([pLangStr isEqualToString:@"fr"]) + { + const std::vector aFR + { @"BE", @"BF", @"BJ", @"CA", @"CH", @"CI", @"FR", @"LU", @"MC", @"ML", + @"MU", @"NE", @"SN", @"TG" }; + for (auto c: aFR) + { + pLangStr = [@"fr_" stringByAppendingString: c]; + postspdict.push_back( pLangStr ); + } + } +#ifdef IOS + else if ([pLangStr isEqualToString:@"fr_FR"]) + { + const std::vector aFR + { @"BE", @"BF", @"BJ", @"CA", @"CH", @"CI", @"FR", @"LU", @"MC", @"ML", + @"MU", @"NE", @"SN", @"TG" }; + for (auto c: aFR) + { + pLangStr = [@"fr_" stringByAppendingString: c]; + postspdict.push_back( pLangStr ); + } + } +#endif + else if ([pLangStr isEqualToString:@"it"]) + { + postspdict.push_back( @"it_CH" ); + postspdict.push_back( @"it_IT" ); + } +#ifdef IOS + else if ([pLangStr isEqualToString:@"it_IT"]) + { + const std::vector aIT + { @"CH", @"IT" }; + for (auto c: aIT) + { + pLangStr = [@"it_" stringByAppendingString: c]; + postspdict.push_back( pLangStr ); + } + } +#endif + else if ([pLangStr isEqualToString:@"ko"]) + { + postspdict.push_back( @"ko_KR" ); + } + else if ([pLangStr isEqualToString:@"nl"]) + { + postspdict.push_back( @"nl_BE" ); + postspdict.push_back( @"nl_NL" ); + } + else if ([pLangStr isEqualToString:@"nb"]) + { + postspdict.push_back( @"nb_NO" ); + } + else if ([pLangStr isEqualToString:@"pl"]) + { + postspdict.push_back( @"pl_PL" ); + } + else if ([pLangStr isEqualToString:@"ru"]) + { + postspdict.push_back( @"ru_RU" ); + } + else if ([pLangStr isEqualToString:@"sv"]) + { + postspdict.push_back( @"sv_FI" ); + postspdict.push_back( @"sv_SE" ); + } +#ifdef IOS + else if ([pLangStr isEqualToString:@"sv_SE"]) + { + postspdict.push_back( @"sv_FI" ); + postspdict.push_back( @"sv_SE" ); + } +#endif + else if ([pLangStr isEqualToString:@"tr"]) + { + postspdict.push_back( @"tr_TR" ); + } + else + postspdict.push_back( pLangStr ); + } + // System has pt_BR and pt_PT, add pt_AO. + postspdict.push_back( @"pt_AO" ); + + numshr = postspdict.size(); + + // we really should merge these and remove duplicates but since + // users can name their dictionaries anything they want it would + // be impossible to know if a real duplication exists unless we + // add some unique key to each myspell dictionary + numdict = numshr; + + if (numdict) { + aDLocs = new Locale [numdict]; + aDEncs = new rtl_TextEncoding [numdict]; + aDNames = new OUString [numdict]; + aSuppLocales.realloc(numdict); + Locale * pLocale = aSuppLocales.getArray(); + int numlocs = 0; + int newloc; + int i,j; + int k = 0; + + //first add the user dictionaries + //TODO for MAC? + + // now add the shared dictionaries + for (i = 0; i < numshr; i++) { + NSDictionary *aLocDict = [ NSLocale componentsFromLocaleIdentifier:postspdict[i] ]; + NSString* aLang = [ aLocDict objectForKey:NSLocaleLanguageCode ]; + NSString* aCountry = [ aLocDict objectForKey:NSLocaleCountryCode ]; + OUString lang([aLang cStringUsingEncoding: NSUTF8StringEncoding], [aLang length], aEnc); + OUString country([ aCountry cStringUsingEncoding: NSUTF8StringEncoding], [aCountry length], aEnc); + Locale nLoc( lang, country, OUString() ); + newloc = 1; + //eliminate duplicates (is this needed for MacOS?) + for (j = 0; j < numlocs; j++) { + if (nLoc == pLocale[j]) newloc = 0; + } + if (newloc) { + pLocale[numlocs] = nLoc; + numlocs++; + } + aDLocs[k] = nLoc; + aDEncs[k] = 0; + k++; + } + + aSuppLocales.realloc(numlocs); + + } else { + /* no dictionary.lst found so register no dictionaries */ + numdict = 0; + aDEncs = nullptr; + aDLocs = nullptr; + aDNames = nullptr; + aSuppLocales.realloc(0); + } + } + + return aSuppLocales; +} + + + +sal_Bool SAL_CALL MacSpellChecker::hasLocale(const Locale& rLocale) +{ + MutexGuard aGuard( GetLinguMutex() ); + + bool bRes = false; + if (!aSuppLocales.getLength()) + getLocales(); + + sal_Int32 nLen = aSuppLocales.getLength(); + for (sal_Int32 i = 0; i < nLen; ++i) + { + const Locale *pLocale = aSuppLocales.getConstArray(); + if (rLocale == pLocale[i]) + { + bRes = true; + break; + } + } + return bRes; +} + + +sal_Int16 MacSpellChecker::GetSpellFailure( const OUString &rWord, const Locale &rLocale ) +{ + // initialize a myspell object for each dictionary once + // (note: mutex is held higher up in isValid) + + + sal_Int16 nRes = -1; + + // first handle smart quotes both single and double + OUStringBuffer rBuf(rWord); + sal_Int32 n = rBuf.getLength(); + sal_Unicode c; + for (sal_Int32 ix=0; ix < n; ix++) { + c = rBuf[ix]; + if ((c == 0x201C) || (c == 0x201D)) rBuf[ix] = u'"'; + if ((c == 0x2018) || (c == 0x2019)) rBuf[ix] = u'\''; + } + OUString nWord(rBuf.makeStringAndClear()); + + if (n) + { + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + NSString* aNSStr = [[[NSString alloc] initWithCharacters: reinterpret_cast(nWord.getStr()) length: nWord.getLength()]autorelease]; + NSString* aLang = [[[NSString alloc] initWithCharacters: reinterpret_cast(rLocale.Language.getStr()) length: rLocale.Language.getLength()]autorelease]; + if(rLocale.Country.getLength()>0) + { + NSString* aCountry = [[[NSString alloc] initWithCharacters: reinterpret_cast(rLocale.Country.getStr()) length: rLocale.Country.getLength()]autorelease]; + NSString* aTaggedCountry = [@"_" stringByAppendingString:aCountry]; + aLang = [aLang stringByAppendingString:aTaggedCountry]; + } + +#ifdef MACOSX + NSInteger aCount; + NSRange range = [[NSSpellChecker sharedSpellChecker] checkSpellingOfString:aNSStr startingAt:0 language:aLang wrap:false inSpellDocumentWithTag:macTag wordCount:&aCount]; +#else + NSRange range = [pChecker rangeOfMisspelledWordInString:aNSStr range:NSMakeRange(0, [aNSStr length]) startingAt:0 wrap:NO language:aLang]; +#endif + int rVal = 0; + if(range.length>0) + { + rVal = -1; + } + else + { + rVal = 1; + } + [pool release]; + if (rVal != 1) + { + nRes = SpellFailure::SPELLING_ERROR; + } else { + return -1; + } + } + return nRes; +} + + + +sal_Bool SAL_CALL + MacSpellChecker::isValid( const OUString& rWord, const Locale& rLocale, + const css::uno::Sequence& rProperties ) +{ + MutexGuard aGuard( GetLinguMutex() ); + + if (rLocale == Locale() || !rWord.getLength()) + return true; + + if (!hasLocale( rLocale )) + return true; + + // Get property values to be used. + // These are be the default values set in the SN_LINGU_PROPERTIES + // PropertySet which are overridden by the supplied ones from the + // last argument. + // You'll probably like to use a simpler solution than the provided + // one using the PropertyHelper_Spell. + + PropertyHelper_Spell &rHelper = GetPropHelper(); + rHelper.SetTmpPropVals( rProperties ); + + sal_Int16 nFailure = GetSpellFailure( rWord, rLocale ); + if (nFailure != -1) + { + LanguageType nLang = LinguLocaleToLanguage( rLocale ); + // postprocess result for errors that should be ignored + if ( (!rHelper.IsSpellUpperCase() && IsUpper( rWord, nLang )) + || (!rHelper.IsSpellWithDigits() && HasDigits( rWord )) + || (!rHelper.IsSpellCapitalization() + && nFailure == SpellFailure::CAPTION_ERROR) + ) + nFailure = -1; + } + + return (nFailure == -1); +} + +Reference< XSpellAlternatives > + MacSpellChecker::GetProposals( const OUString &rWord, const Locale &rLocale ) +{ + // Retrieves the return values for the 'spell' function call in case + // of a misspelled word. + // Especially it may give a list of suggested (correct) words: + + Reference< XSpellAlternatives > xRes; + // note: mutex is held by higher up by spell which covers both + + LanguageType nLang = LinguLocaleToLanguage( rLocale ); + int count; + Sequence< OUString > aStr( 0 ); + + // first handle smart quotes (single and double) + OUStringBuffer rBuf(rWord); + sal_Int32 n = rBuf.getLength(); + sal_Unicode c; + for (sal_Int32 ix=0; ix < n; ix++) { + c = rBuf[ix]; + if ((c == 0x201C) || (c == 0x201D)) rBuf[ix] = u'"'; + if ((c == 0x2018) || (c == 0x2019)) rBuf[ix] = u'\''; + } + OUString nWord(rBuf.makeStringAndClear()); + + if (n) + { + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + NSString* aNSStr = [[[NSString alloc] initWithCharacters: reinterpret_cast(nWord.getStr()) length: nWord.getLength()]autorelease]; + NSString* aLang = [[[NSString alloc] initWithCharacters: reinterpret_cast(rLocale.Language.getStr()) length: rLocale.Language.getLength()]autorelease]; + if(rLocale.Country.getLength()>0) + { + NSString* aCountry = [[[NSString alloc] initWithCharacters: reinterpret_cast(rLocale.Country.getStr()) length: rLocale.Country.getLength()]autorelease]; + NSString* aTaggedCountry = [@"_" stringByAppendingString:aCountry]; + aLang = [aLang stringByAppendingString:aTaggedCountry]; + } +#ifdef MACOSX + [[NSSpellChecker sharedSpellChecker] setLanguage:aLang]; + NSArray *guesses = [[NSSpellChecker sharedSpellChecker] guessesForWordRange:NSMakeRange(0, [aNSStr length]) inString:aNSStr language:aLang inSpellDocumentWithTag:0]; + (void) this; // avoid loplugin:staticmethods, the !MACOSX case uses 'this' +#else + NSArray *guesses = [pChecker guessesForWordRange:NSMakeRange(0, [aNSStr length]) inString:aNSStr language:aLang]; +#endif + count = [guesses count]; + if (count) + { + aStr.realloc( count ); + OUString *pStr = aStr.getArray(); + for (int ii=0; ii < count; ii++) + { + // if needed add: if (suglst[ii] == NULL) continue; + NSString* guess = [guesses objectAtIndex:ii]; + OUString cvtwrd(reinterpret_cast([guess cStringUsingEncoding:NSUnicodeStringEncoding]), static_cast([guess length])); + pStr[ii] = cvtwrd; + } + } + [pool release]; + } + + // now return an empty alternative for no suggestions or the list of alternatives if some found + rtl::Reference pAlt = new SpellAlternatives; + pAlt->SetWordLanguage( rWord, nLang ); + pAlt->SetFailureType( SpellFailure::SPELLING_ERROR ); + pAlt->SetAlternatives( aStr ); + xRes = pAlt; + return xRes; + +} + +Reference< XSpellAlternatives > SAL_CALL + MacSpellChecker::spell( const OUString& rWord, const Locale& rLocale, + const css::uno::Sequence& rProperties ) +{ + MutexGuard aGuard( GetLinguMutex() ); + + if (rLocale == Locale() || !rWord.getLength()) + return nullptr; + + if (!hasLocale( rLocale )) + return nullptr; + + Reference< XSpellAlternatives > xAlt; + if (!isValid( rWord, rLocale, rProperties )) + { + xAlt = GetProposals( rWord, rLocale ); + } + return xAlt; +} + +sal_Bool SAL_CALL + MacSpellChecker::addLinguServiceEventListener( + const Reference< XLinguServiceEventListener >& rxLstnr ) +{ + MutexGuard aGuard( GetLinguMutex() ); + + bool bRes = false; + if (!bDisposing && rxLstnr.is()) + { + bRes = GetPropHelper().addLinguServiceEventListener( rxLstnr ); + } + return bRes; +} + + +sal_Bool SAL_CALL + MacSpellChecker::removeLinguServiceEventListener( + const Reference< XLinguServiceEventListener >& rxLstnr ) +{ + MutexGuard aGuard( GetLinguMutex() ); + + bool bRes = false; + if (!bDisposing && rxLstnr.is()) + { + DBG_ASSERT( xPropHelper.is(), "xPropHelper non existent" ); + bRes = GetPropHelper().removeLinguServiceEventListener( rxLstnr ); + } + return bRes; +} + + +OUString SAL_CALL + MacSpellChecker::getServiceDisplayName( const Locale& /*rLocale*/ ) +{ + MutexGuard aGuard( GetLinguMutex() ); + return "macOS Spell Checker"; +} + + +void SAL_CALL + MacSpellChecker::initialize( const Sequence< Any >& rArguments ) +{ + MutexGuard aGuard( GetLinguMutex() ); + + if (!xPropHelper.is()) + { + sal_Int32 nLen = rArguments.getLength(); + if (2 == nLen) + { + Reference< XLinguProperties > xPropSet; + rArguments.getConstArray()[0] >>= xPropSet; + //rArguments.getConstArray()[1] >>= xDicList; + + //! Pointer allows for access of the non-UNO functions. + //! And the reference to the UNO-functions while increasing + //! the ref-count and will implicitly free the memory + //! when the object is no longer used. + xPropHelper = new PropertyHelper_Spell( static_cast(this), xPropSet ); + xPropHelper->AddAsPropListener(); + } + else + OSL_FAIL( "wrong number of arguments in sequence" ); + + } +} + + +void SAL_CALL + MacSpellChecker::dispose() +{ + MutexGuard aGuard( GetLinguMutex() ); + + if (!bDisposing) + { + bDisposing = true; + EventObject aEvtObj( static_cast(this) ); + aEvtListeners.disposeAndClear( aEvtObj ); + } +} + + +void SAL_CALL + MacSpellChecker::addEventListener( const Reference< XEventListener >& rxListener ) +{ + MutexGuard aGuard( GetLinguMutex() ); + + if (!bDisposing && rxListener.is()) + aEvtListeners.addInterface( rxListener ); +} + + +void SAL_CALL + MacSpellChecker::removeEventListener( const Reference< XEventListener >& rxListener ) +{ + MutexGuard aGuard( GetLinguMutex() ); + + if (!bDisposing && rxListener.is()) + aEvtListeners.removeInterface( rxListener ); +} + +// Service specific part +OUString SAL_CALL MacSpellChecker::getImplementationName() +{ + return "org.openoffice.lingu.MacOSXSpellChecker"; +} + +sal_Bool SAL_CALL MacSpellChecker::supportsService( const OUString& ServiceName ) +{ + return cppu::supportsService(this, ServiceName); +} + +Sequence< OUString > SAL_CALL MacSpellChecker::getSupportedServiceNames() +{ + return { SN_SPELLCHECKER }; +} + +extern "C" SAL_DLLPUBLIC_EXPORT css::uno::XInterface* +lingucomponent_MacSpellChecker_get_implementation( + css::uno::XComponentContext* , css::uno::Sequence const&) +{ + return cppu::acquire(new MacSpellChecker()); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/lingucomponent/source/spellcheck/spell/spell.component b/lingucomponent/source/spellcheck/spell/spell.component new file mode 100644 index 000000000..c284e13fc --- /dev/null +++ b/lingucomponent/source/spellcheck/spell/spell.component @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/lingucomponent/source/spellcheck/spell/sspellimp.cxx b/lingucomponent/source/spellcheck/spell/sspellimp.cxx new file mode 100644 index 000000000..95b264157 --- /dev/null +++ b/lingucomponent/source/spellcheck/spell/sspellimp.cxx @@ -0,0 +1,635 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "sspellimp.hxx" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace utl; +using namespace osl; +using namespace com::sun::star; +using namespace com::sun::star::beans; +using namespace com::sun::star::lang; +using namespace com::sun::star::uno; +using namespace com::sun::star::linguistic2; +using namespace linguistic; + +// XML-header of SPELLML queries +#if !defined SPELL_XML +constexpr OUStringLiteral SPELL_XML = u""; +#endif + +// only available in hunspell >= 1.5 +#if !defined MAXWORDLEN +#define MAXWORDLEN 176 +#endif + +SpellChecker::SpellChecker() : + m_aEvtListeners(GetLinguMutex()), + m_bDisposing(false) +{ +} + +SpellChecker::DictItem::DictItem(OUString i_DName, Locale i_DLoc, rtl_TextEncoding i_DEnc) + : m_aDName(std::move(i_DName)) + , m_aDLoc(std::move(i_DLoc)) + , m_aDEnc(i_DEnc) +{ +} + +SpellChecker::~SpellChecker() +{ + if (m_pPropHelper) + { + m_pPropHelper->RemoveAsPropListener(); + } +} + +PropertyHelper_Spelling & SpellChecker::GetPropHelper_Impl() +{ + if (!m_pPropHelper) + { + Reference< XLinguProperties > xPropSet = GetLinguProperties(); + + m_pPropHelper.reset( new PropertyHelper_Spelling( static_cast(this), xPropSet ) ); + m_pPropHelper->AddAsPropListener(); //! after a reference is established + } + return *m_pPropHelper; +} + +Sequence< Locale > SAL_CALL SpellChecker::getLocales() +{ + MutexGuard aGuard( GetLinguMutex() ); + + // this routine should return the locales supported by the installed + // dictionaries. + if (m_DictItems.empty()) + { + SvtLinguConfig aLinguCfg; + + // get list of extension dictionaries-to-use + // (or better speaking: the list of dictionaries using the + // new configuration entries). + std::vector< SvtLinguConfigDictionaryEntry > aDics; + uno::Sequence< OUString > aFormatList; + aLinguCfg.GetSupportedDictionaryFormatsFor( "SpellCheckers", + "org.openoffice.lingu.MySpellSpellChecker", aFormatList ); + for (auto const& format : std::as_const(aFormatList)) + { + std::vector< SvtLinguConfigDictionaryEntry > aTmpDic( + aLinguCfg.GetActiveDictionariesByFormat(format) ); + aDics.insert( aDics.end(), aTmpDic.begin(), aTmpDic.end() ); + } + + //!! for compatibility with old dictionaries (the ones not using extensions + //!! or new configuration entries, but still using the dictionary.lst file) + //!! Get the list of old style spell checking dictionaries to use... + std::vector< SvtLinguConfigDictionaryEntry > aOldStyleDics( + GetOldStyleDics( "DICT" ) ); + + // to prefer dictionaries with configuration entries we will only + // use those old style dictionaries that add a language that + // is not yet supported by the list of new style dictionaries + MergeNewStyleDicsAndOldStyleDics( aDics, aOldStyleDics ); + + if (!aDics.empty()) + { + uno::Reference< lang::XMultiServiceFactory > xServiceFactory(comphelper::getProcessServiceFactory()); + uno::Reference< ucb::XSimpleFileAccess > xAccess(xServiceFactory->createInstance("com.sun.star.ucb.SimpleFileAccess"), uno::UNO_QUERY); + // get supported locales from the dictionaries-to-use... + std::set aLocaleNamesSet; + for (auto const& dict : aDics) + { + const uno::Sequence< OUString > aLocaleNames( dict.aLocaleNames ); + uno::Sequence< OUString > aLocations( dict.aLocations ); + SAL_WARN_IF( + aLocaleNames.hasElements() && !aLocations.hasElements(), + "lingucomponent", "no locations"); + if (aLocations.hasElements()) + { + if (xAccess.is() && xAccess->exists(aLocations[0])) + { + for (auto const& locale : aLocaleNames) + { + if (!comphelper::LibreOfficeKit::isAllowlistedLanguage(locale)) + continue; + + aLocaleNamesSet.insert(locale); + } + } + else + { + SAL_WARN( + "lingucomponent", + "missing <" << aLocations[0] << ">"); + } + } + } + // ... and add them to the resulting sequence + m_aSuppLocales.realloc( aLocaleNamesSet.size() ); + std::transform( + aLocaleNamesSet.begin(), aLocaleNamesSet.end(), m_aSuppLocales.getArray(), + [](auto const& localeName) { return LanguageTag::convertToLocale(localeName); }); + + //! For each dictionary and each locale we need a separate entry. + //! If this results in more than one dictionary per locale than (for now) + //! it is undefined which dictionary gets used. + //! In the future the implementation should support using several dictionaries + //! for one locale. + sal_uInt32 nDictSize = std::accumulate(aDics.begin(), aDics.end(), sal_uInt32(0), + [](const sal_uInt32 nSum, const SvtLinguConfigDictionaryEntry& dict) { + return nSum + dict.aLocaleNames.getLength(); }); + + // add dictionary information + m_DictItems.reserve(nDictSize); + for (auto const& dict : aDics) + { + if (dict.aLocaleNames.hasElements() && + dict.aLocations.hasElements()) + { + const uno::Sequence< OUString > aLocaleNames( dict.aLocaleNames ); + + // currently only one language per dictionary is supported in the actual implementation... + // Thus here we work-around this by adding the same dictionary several times. + // Once for each of its supported locales. + for (auto const& localeName : aLocaleNames) + { + // also both files have to be in the same directory and the + // file names must only differ in the extension (.aff/.dic). + // Thus we use the first location only and strip the extension part. + OUString aLocation = dict.aLocations[0]; + sal_Int32 nPos = aLocation.lastIndexOf( '.' ); + aLocation = aLocation.copy( 0, nPos ); + + m_DictItems.emplace_back(aLocation, LanguageTag::convertToLocale(localeName), RTL_TEXTENCODING_DONTKNOW); + } + } + } + DBG_ASSERT( nDictSize == m_DictItems.size(), "index mismatch?" ); + } + else + { + // no dictionary found so register no dictionaries + m_aSuppLocales.realloc(0); + } + } + + return m_aSuppLocales; +} + +sal_Bool SAL_CALL SpellChecker::hasLocale(const Locale& rLocale) +{ + MutexGuard aGuard( GetLinguMutex() ); + + bool bRes = false; + if (!m_aSuppLocales.hasElements()) + getLocales(); + + for (auto const& suppLocale : std::as_const(m_aSuppLocales)) + { + if (rLocale == suppLocale) + { + bRes = true; + break; + } + } + return bRes; +} + +sal_Int16 SpellChecker::GetSpellFailure(const OUString &rWord, const Locale &rLocale) +{ + if (rWord.getLength() > MAXWORDLEN) + return -1; + + Hunspell * pMS = nullptr; + rtl_TextEncoding eEnc = RTL_TEXTENCODING_DONTKNOW; + + // initialize a myspell object for each dictionary once + // (note: mutex is held higher up in isValid) + + sal_Int16 nRes = -1; + + // first handle smart quotes both single and double + OUStringBuffer rBuf(rWord); + sal_Int32 n = rBuf.getLength(); + sal_Unicode c; + sal_Int32 extrachar = 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)) + rBuf[ix] = u'\''; + + // 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.) + else if ((c == 0x200C) || (c == 0x200D) || + ((c >= 0xFB00) && (c <= 0xFB04))) + extrachar = 1; + } + OUString nWord(rBuf.makeStringAndClear()); + + if (n) + { + for (auto& currDict : m_DictItems) + { + pMS = nullptr; + eEnc = RTL_TEXTENCODING_DONTKNOW; + + if (rLocale == currDict.m_aDLoc) + { + if (!currDict.m_pDict) + { + OUString dicpath = currDict.m_aDName + ".dic"; + OUString affpath = currDict.m_aDName + ".aff"; + OUString dict; + OUString aff; + osl::FileBase::getSystemPathFromFileURL(dicpath,dict); + osl::FileBase::getSystemPathFromFileURL(affpath,aff); +#if defined(_WIN32) + // workaround for Windows specific problem that the + // path length in calls to 'fopen' is limited to somewhat + // about 120+ characters which will usually be exceed when + // using dictionaries as extensions. (Hunspell waits UTF-8 encoded + // path with \\?\ long path prefix.) + OString aTmpaff = Win_AddLongPathPrefix(OUStringToOString(aff, RTL_TEXTENCODING_UTF8)); + OString aTmpdict = Win_AddLongPathPrefix(OUStringToOString(dict, RTL_TEXTENCODING_UTF8)); +#else + OString aTmpaff(OU2ENC(aff,osl_getThreadTextEncoding())); + OString aTmpdict(OU2ENC(dict,osl_getThreadTextEncoding())); +#endif + + currDict.m_pDict = std::make_unique(aTmpaff.getStr(),aTmpdict.getStr()); +#if defined(H_DEPRECATED) + currDict.m_aDEnc = getTextEncodingFromCharset(currDict.m_pDict->get_dict_encoding().c_str()); +#else + currDict.m_aDEnc = getTextEncodingFromCharset(currDict.m_pDict->get_dic_encoding()); +#endif + } + pMS = currDict.m_pDict.get(); + eEnc = currDict.m_aDEnc; + } + + if (pMS) + { + // we don't want to work with a default text encoding since following incorrect + // results may occur only for specific text and thus may be hard to notice. + // Thus better always make a clean exit here if the text encoding is in question. + // Hopefully something not working at all will raise proper attention quickly. ;-) + DBG_ASSERT( eEnc != RTL_TEXTENCODING_DONTKNOW, "failed to get text encoding! (maybe incorrect encoding string in file)" ); + if (eEnc == RTL_TEXTENCODING_DONTKNOW) + return -1; + + OString aWrd(OU2ENC(nWord,eEnc)); +#if defined(H_DEPRECATED) + bool bVal = pMS->spell(std::string(aWrd.getStr())); +#else + bool bVal = pMS->spell(aWrd.getStr()) != 0; +#endif + if (!bVal) { + if (extrachar && (eEnc != RTL_TEXTENCODING_UTF8)) { + OUStringBuffer aBuf(nWord); + n = aBuf.getLength(); + for (sal_Int32 ix=n-1; ix >= 0; ix--) + { + switch (aBuf[ix]) { + case 0xFB00: aBuf.remove(ix, 1); aBuf.insert(ix, "ff"); break; + case 0xFB01: aBuf.remove(ix, 1); aBuf.insert(ix, "fi"); break; + case 0xFB02: aBuf.remove(ix, 1); aBuf.insert(ix, "fl"); break; + case 0xFB03: aBuf.remove(ix, 1); aBuf.insert(ix, "ffi"); break; + case 0xFB04: aBuf.remove(ix, 1); aBuf.insert(ix, "ffl"); break; + case 0x200C: + case 0x200D: aBuf.remove(ix, 1); break; + } + } + OUString aWord(aBuf.makeStringAndClear()); + OString bWrd(OU2ENC(aWord, eEnc)); +#if defined(H_DEPRECATED) + bVal = pMS->spell(std::string(bWrd.getStr())); +#else + bVal = pMS->spell(bWrd.getStr()) != 0; +#endif + if (bVal) return -1; + } + nRes = SpellFailure::SPELLING_ERROR; + } else { + return -1; + } + pMS = nullptr; + } + } + } + + return nRes; +} + +sal_Bool SAL_CALL SpellChecker::isValid( const OUString& rWord, const Locale& rLocale, + const css::uno::Sequence< css::beans::PropertyValue >& rProperties ) +{ + MutexGuard aGuard( GetLinguMutex() ); + + if (rLocale == Locale() || rWord.isEmpty()) + return true; + + if (!hasLocale( rLocale )) + return true; + + // return sal_False to process SPELLML requests (they are longer than the header) + if (rWord.match(SPELL_XML, 0) && (rWord.getLength() > 10)) return false; + + // Get property values to be used. + // These are be the default values set in the SN_LINGU_PROPERTIES + // PropertySet which are overridden by the supplied ones from the + // last argument. + // You'll probably like to use a simpler solution than the provided + // one using the PropertyHelper_Spell. + PropertyHelper_Spelling& rHelper = GetPropHelper(); + rHelper.SetTmpPropVals( rProperties ); + + sal_Int16 nFailure = GetSpellFailure( rWord, rLocale ); + if (nFailure != -1 && !rWord.match(SPELL_XML, 0)) + { + LanguageType nLang = LinguLocaleToLanguage( rLocale ); + // postprocess result for errors that should be ignored + const bool bIgnoreError = + (!rHelper.IsSpellUpperCase() && IsUpper( rWord, nLang )) || + (!rHelper.IsSpellWithDigits() && HasDigits( rWord )) || + (!rHelper.IsSpellCapitalization() && nFailure == SpellFailure::CAPTION_ERROR); + if (bIgnoreError) + nFailure = -1; + } + + return (nFailure == -1); +} + +Reference< XSpellAlternatives > + SpellChecker::GetProposals( const OUString &rWord, const Locale &rLocale ) +{ + // Retrieves the return values for the 'spell' function call in case + // of a misspelled word. + // Especially it may give a list of suggested (correct) words: + Reference< XSpellAlternatives > xRes; + // note: mutex is held by higher up by spell which covers both + + Hunspell* pMS = nullptr; + rtl_TextEncoding eEnc = RTL_TEXTENCODING_DONTKNOW; + + // first handle smart quotes (single and double) + OUStringBuffer rBuf(rWord); + sal_Int32 n = rBuf.getLength(); + sal_Unicode c; + for (sal_Int32 ix=0; ix < n; ix++) + { + c = rBuf[ix]; + if ((c == 0x201C) || (c == 0x201D)) + rBuf[ix] = u'"'; + if ((c == 0x2018) || (c == 0x2019)) + rBuf[ix] = u'\''; + } + OUString nWord(rBuf.makeStringAndClear()); + + if (n) + { + LanguageType nLang = LinguLocaleToLanguage( rLocale ); + int numsug = 0; + + Sequence< OUString > aStr( 0 ); + for (const auto& currDict : m_DictItems) + { + pMS = nullptr; + eEnc = RTL_TEXTENCODING_DONTKNOW; + + if (rLocale == currDict.m_aDLoc) + { + pMS = currDict.m_pDict.get(); + eEnc = currDict.m_aDEnc; + } + + if (pMS) + { + OString aWrd(OU2ENC(nWord,eEnc)); +#if defined(H_DEPRECATED) + std::vector suglst = pMS->suggest(std::string(aWrd.getStr())); + if (!suglst.empty()) + { + aStr.realloc(numsug + suglst.size()); + OUString *pStr = aStr.getArray(); + for (size_t ii = 0; ii < suglst.size(); ++ii) + { + OUString cvtwrd(suglst[ii].c_str(), suglst[ii].size(), eEnc); + pStr[numsug + ii] = cvtwrd; + } + numsug += suglst.size(); + } +#else + char ** suglst = nullptr; + int count = pMS->suggest(&suglst, aWrd.getStr()); + if (count) + { + aStr.realloc( numsug + count ); + OUString *pStr = aStr.getArray(); + for (int ii=0; ii < count; ++ii) + { + OUString cvtwrd(suglst[ii],strlen(suglst[ii]),eEnc); + pStr[numsug + ii] = cvtwrd; + } + numsug += count; + } + pMS->free_list(&suglst, count); +#endif + } + } + + // now return an empty alternative for no suggestions or the list of alternatives if some found + xRes = SpellAlternatives::CreateSpellAlternatives( rWord, nLang, SpellFailure::SPELLING_ERROR, aStr ); + return xRes; + } + return xRes; +} + +Reference< XSpellAlternatives > SAL_CALL SpellChecker::spell( + const OUString& rWord, const Locale& rLocale, + const css::uno::Sequence< css::beans::PropertyValue >& rProperties ) +{ + MutexGuard aGuard( GetLinguMutex() ); + + if (rLocale == Locale() || rWord.isEmpty()) + return nullptr; + + if (!hasLocale( rLocale )) + return nullptr; + + Reference< XSpellAlternatives > xAlt; + if (!isValid( rWord, rLocale, rProperties )) + { + xAlt = GetProposals( rWord, rLocale ); + } + return xAlt; +} + +sal_Bool SAL_CALL SpellChecker::addLinguServiceEventListener( + const Reference< XLinguServiceEventListener >& rxLstnr ) +{ + MutexGuard aGuard( GetLinguMutex() ); + + bool bRes = false; + if (!m_bDisposing && rxLstnr.is()) + { + bRes = GetPropHelper().addLinguServiceEventListener( rxLstnr ); + } + return bRes; +} + +sal_Bool SAL_CALL SpellChecker::removeLinguServiceEventListener( + const Reference< XLinguServiceEventListener >& rxLstnr ) +{ + MutexGuard aGuard( GetLinguMutex() ); + + bool bRes = false; + if (!m_bDisposing && rxLstnr.is()) + { + bRes = GetPropHelper().removeLinguServiceEventListener( rxLstnr ); + } + return bRes; +} + +OUString SAL_CALL SpellChecker::getServiceDisplayName(const Locale& rLocale) +{ + std::locale loc(Translate::Create("svt", LanguageTag(rLocale))); + return Translate::get(STR_DESCRIPTION_HUNSPELL, loc); +} + +void SAL_CALL SpellChecker::initialize( const Sequence< Any >& rArguments ) +{ + MutexGuard aGuard( GetLinguMutex() ); + + if (m_pPropHelper) + return; + + sal_Int32 nLen = rArguments.getLength(); + if (2 == nLen) + { + Reference< XLinguProperties > xPropSet; + rArguments.getConstArray()[0] >>= xPropSet; + // rArguments.getConstArray()[1] >>= xDicList; + + //! Pointer allows for access of the non-UNO functions. + //! And the reference to the UNO-functions while increasing + //! the ref-count and will implicitly free the memory + //! when the object is no longer used. + m_pPropHelper.reset( new PropertyHelper_Spelling( static_cast(this), xPropSet ) ); + m_pPropHelper->AddAsPropListener(); //! after a reference is established + } + else { + OSL_FAIL( "wrong number of arguments in sequence" ); + } +} + +void SAL_CALL SpellChecker::dispose() +{ + MutexGuard aGuard( GetLinguMutex() ); + + if (!m_bDisposing) + { + m_bDisposing = true; + EventObject aEvtObj( static_cast(this) ); + m_aEvtListeners.disposeAndClear( aEvtObj ); + if (m_pPropHelper) + { + m_pPropHelper->RemoveAsPropListener(); + m_pPropHelper.reset(); + } + } +} + +void SAL_CALL SpellChecker::addEventListener( const Reference< XEventListener >& rxListener ) +{ + MutexGuard aGuard( GetLinguMutex() ); + + if (!m_bDisposing && rxListener.is()) + m_aEvtListeners.addInterface( rxListener ); +} + +void SAL_CALL SpellChecker::removeEventListener( const Reference< XEventListener >& rxListener ) +{ + MutexGuard aGuard( GetLinguMutex() ); + + if (!m_bDisposing && rxListener.is()) + m_aEvtListeners.removeInterface( rxListener ); +} + +// Service specific part +OUString SAL_CALL SpellChecker::getImplementationName() +{ + return "org.openoffice.lingu.MySpellSpellChecker"; +} + +sal_Bool SAL_CALL SpellChecker::supportsService( const OUString& ServiceName ) +{ + return cppu::supportsService(this, ServiceName); +} + +Sequence< OUString > SAL_CALL SpellChecker::getSupportedServiceNames() +{ + return { SN_SPELLCHECKER }; +} + +extern "C" SAL_DLLPUBLIC_EXPORT css::uno::XInterface* +lingucomponent_SpellChecker_get_implementation( + css::uno::XComponentContext* , css::uno::Sequence const&) +{ + return cppu::acquire(new SpellChecker()); +} + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/lingucomponent/source/spellcheck/spell/sspellimp.hxx b/lingucomponent/source/spellcheck/spell/sspellimp.hxx new file mode 100644 index 000000000..000f1756f --- /dev/null +++ b/lingucomponent/source/spellcheck/spell/sspellimp.hxx @@ -0,0 +1,120 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_LINGUCOMPONENT_SOURCE_SPELLCHECK_SPELL_SSPELLIMP_HXX +#define INCLUDED_LINGUCOMPONENT_SOURCE_SPELLCHECK_SPELL_SSPELLIMP_HXX + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::beans; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::linguistic2; + +class SpellChecker : + public cppu::WeakImplHelper + < + XSpellChecker, + XLinguServiceEventBroadcaster, + XInitialization, + XComponent, + XServiceInfo, + XServiceDisplayName + > +{ + struct DictItem + { + OUString m_aDName; + Locale m_aDLoc; + std::unique_ptr m_pDict; + rtl_TextEncoding m_aDEnc; + + DictItem(OUString i_DName, Locale i_DLoc, rtl_TextEncoding i_DEnc); + }; + + std::vector m_DictItems; + + Sequence< Locale > m_aSuppLocales; + + ::comphelper::OInterfaceContainerHelper3 m_aEvtListeners; + std::unique_ptr m_pPropHelper; + bool m_bDisposing; + + SpellChecker(const SpellChecker &) = delete; + SpellChecker & operator = (const SpellChecker &) = delete; + + linguistic::PropertyHelper_Spelling& GetPropHelper_Impl(); + linguistic::PropertyHelper_Spelling& GetPropHelper() + { + return m_pPropHelper ? *m_pPropHelper : GetPropHelper_Impl(); + } + + sal_Int16 GetSpellFailure( const OUString &rWord, const Locale &rLocale ); + Reference< XSpellAlternatives > GetProposals( const OUString &rWord, const Locale &rLocale ); + +public: + SpellChecker(); + virtual ~SpellChecker() override; + + // XSupportedLocales (for XSpellChecker) + virtual Sequence< Locale > SAL_CALL getLocales() override; + virtual sal_Bool SAL_CALL hasLocale( const Locale& rLocale ) override; + + // XSpellChecker + virtual sal_Bool SAL_CALL isValid( const OUString& rWord, const Locale& rLocale, const css::uno::Sequence< css::beans::PropertyValue >& rProperties ) override; + virtual Reference< XSpellAlternatives > SAL_CALL spell( const OUString& rWord, const Locale& rLocale, const css::uno::Sequence< css::beans::PropertyValue >& rProperties ) override; + + // XLinguServiceEventBroadcaster + virtual sal_Bool SAL_CALL addLinguServiceEventListener( const Reference< XLinguServiceEventListener >& rxLstnr ) override; + virtual sal_Bool SAL_CALL removeLinguServiceEventListener( const Reference< XLinguServiceEventListener >& rxLstnr ) override; + + // XServiceDisplayName + virtual OUString SAL_CALL getServiceDisplayName( const Locale& rLocale ) override; + + // XInitialization + virtual void SAL_CALL initialize( const Sequence< Any >& rArguments ) override; + + // XComponent + virtual void SAL_CALL dispose() override; + virtual void SAL_CALL addEventListener( const Reference< XEventListener >& rxListener ) override; + virtual void SAL_CALL removeEventListener( const Reference< XEventListener >& rxListener ) override; + + // XServiceInfo + virtual OUString SAL_CALL getImplementationName() override; + virtual sal_Bool SAL_CALL supportsService( const OUString& rServiceName ) override; + virtual Sequence< OUString > SAL_CALL getSupportedServiceNames() override; +}; + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ -- cgit v1.2.3