summaryrefslogtreecommitdiffstats
path: root/lingucomponent/source/spellcheck
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 05:54:39 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 05:54:39 +0000
commit267c6f2ac71f92999e969232431ba04678e7437e (patch)
tree358c9467650e1d0a1d7227a21dac2e3d08b622b2 /lingucomponent/source/spellcheck
parentInitial commit. (diff)
downloadlibreoffice-267c6f2ac71f92999e969232431ba04678e7437e.tar.xz
libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.zip
Adding upstream version 4:24.2.0.upstream/4%24.2.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lingucomponent/source/spellcheck')
-rw-r--r--lingucomponent/source/spellcheck/languagetool/LanguageTool.component26
-rw-r--r--lingucomponent/source/spellcheck/languagetool/languagetoolimp.cxx519
-rw-r--r--lingucomponent/source/spellcheck/languagetool/languagetoolimp.hxx75
-rw-r--r--lingucomponent/source/spellcheck/macosxspell/MacOSXSpell.component26
-rw-r--r--lingucomponent/source/spellcheck/macosxspell/macspellimp.hxx123
-rw-r--r--lingucomponent/source/spellcheck/macosxspell/macspellimp.mm678
-rw-r--r--lingucomponent/source/spellcheck/spell/spell.component26
-rw-r--r--lingucomponent/source/spellcheck/spell/sspellimp.cxx648
-rw-r--r--lingucomponent/source/spellcheck/spell/sspellimp.hxx120
9 files changed, 2241 insertions, 0 deletions
diff --git a/lingucomponent/source/spellcheck/languagetool/LanguageTool.component b/lingucomponent/source/spellcheck/languagetool/LanguageTool.component
new file mode 100644
index 0000000000..9f7eb3d087
--- /dev/null
+++ b/lingucomponent/source/spellcheck/languagetool/LanguageTool.component
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * 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 .
+ -->
+
+<component loader="com.sun.star.loader.SharedLibrary" environment="@CPPU_ENV@"
+ xmlns="http://openoffice.org/2010/uno-components">
+ <implementation name="org.openoffice.lingu.LanguageToolGrammarChecker"
+ constructor="lingucomponent_LanguageToolGrammarChecker_get_implementation" single-instance="true">
+ <service name="com.sun.star.linguistic2.Proofreader"/>
+ </implementation>
+</component>
diff --git a/lingucomponent/source/spellcheck/languagetool/languagetoolimp.cxx b/lingucomponent/source/spellcheck/languagetool/languagetoolimp.cxx
new file mode 100644
index 0000000000..fe912cb6b3
--- /dev/null
+++ b/lingucomponent/source/spellcheck/languagetool/languagetoolimp.cxx
@@ -0,0 +1,519 @@
+/* -*- 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 <sal/config.h>
+
+#include <config_version.h>
+
+#include <cppuhelper/factory.hxx>
+#include <cppuhelper/supportsservice.hxx>
+#include <cppuhelper/weak.hxx>
+#include "languagetoolimp.hxx"
+
+#include <i18nlangtag/languagetag.hxx>
+#include <svtools/strings.hrc>
+#include <unotools/resmgr.hxx>
+
+#include <vector>
+#include <set>
+#include <string.h>
+
+#include <officecfg/Office/Linguistic.hxx>
+
+#include <curl/curl.h>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/json_parser.hpp>
+#include <algorithm>
+#include <string_view>
+
+#include <systools/curlinit.hxx>
+
+#include <sal/log.hxx>
+#include <tools/color.hxx>
+#include <tools/long.hxx>
+#include <com/sun/star/text/TextMarkupType.hpp>
+#include <com/sun/star/uno/Any.hxx>
+#include <comphelper/propertyvalue.hxx>
+#include <unotools/lingucfg.hxx>
+#include <osl/mutex.hxx>
+#include <rtl/uri.hxx>
+
+using namespace com::sun::star;
+using namespace com::sun::star::beans;
+using namespace com::sun::star::lang;
+using namespace com::sun::star::linguistic2;
+
+constexpr OUStringLiteral sDuden = u"duden";
+
+namespace
+{
+constexpr size_t MAX_SUGGESTIONS_SIZE = 10;
+using LanguageToolCfg = officecfg::Office::Linguistic::GrammarChecking::LanguageTool;
+
+PropertyValue lcl_GetLineColorPropertyFromErrorId(const std::string& rErrorId)
+{
+ Color aColor;
+ if (rErrorId == "TYPOS" || rErrorId == "orth")
+ {
+ aColor = COL_LIGHTRED;
+ }
+ else if (rErrorId == "STYLE")
+ {
+ aColor = COL_LIGHTBLUE;
+ }
+ else
+ {
+ // Same color is used for other errorId's such as GRAMMAR, TYPOGRAPHY..
+ constexpr Color COL_ORANGE(0xD1, 0x68, 0x20);
+ aColor = COL_ORANGE;
+ }
+ return comphelper::makePropertyValue("LineColor", aColor);
+}
+
+OString encodeTextForLT(const OUString& text)
+{
+ // Let's be a bit conservative. I don't find a good description what needs encoding (and in
+ // which way) at https://languagetool.org/http-api/; the "Try it out!" function shows that
+ // different cases are handled differently by the demo; some percent-encode the UTF-8
+ // representation, like %D0%90 (for cyrillic А); some turn into entities like &#33; (for
+ // exclamation mark !); some other to things like \u0027 (for apostrophe '). So only keep
+ // RFC 3986's "Unreserved Characters" set unencoded, use UTF-8 percent-encoding for the rest.
+ static constexpr auto myCharClass = rtl::createUriCharClass(
+ u8"-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
+ return OUStringToOString(
+ rtl::Uri::encode(text, myCharClass.data(), rtl_UriEncodeStrict, RTL_TEXTENCODING_UTF8),
+ RTL_TEXTENCODING_ASCII_US);
+}
+
+// Callback to get the response data from server.
+size_t WriteCallback(void* ptr, size_t size, size_t nmemb, void* userp)
+{
+ if (!userp)
+ return 0;
+
+ std::string* response = static_cast<std::string*>(userp);
+ size_t real_size = size * nmemb;
+ response->append(static_cast<char*>(ptr), real_size);
+ return real_size;
+}
+
+enum class HTTP_METHOD
+{
+ HTTP_GET,
+ HTTP_POST
+};
+
+std::string makeHttpRequest_impl(std::u16string_view aURL, HTTP_METHOD method,
+ const OString& aPostData, curl_slist* pHttpHeader,
+ tools::Long& nStatusCode)
+{
+ struct curl_cleanup_t
+ {
+ void operator()(CURL* p) const { curl_easy_cleanup(p); }
+ };
+ std::unique_ptr<CURL, curl_cleanup_t> curl(curl_easy_init());
+ if (!curl)
+ {
+ SAL_WARN("languagetool", "CURL initialization failed");
+ return {}; // empty string
+ }
+
+ ::InitCurl_easy(curl.get());
+
+ OString aURL8 = OUStringToOString(aURL, RTL_TEXTENCODING_UTF8);
+ (void)curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, pHttpHeader);
+ (void)curl_easy_setopt(curl.get(), CURLOPT_FAILONERROR, 1L);
+ (void)curl_easy_setopt(curl.get(), CURLOPT_URL, aURL8.getStr());
+ (void)curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, 10L);
+ // (void)curl_easy_setopt(curl.get(), CURLOPT_VERBOSE, 1L);
+
+ std::string response_body;
+ (void)curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, WriteCallback);
+ (void)curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response_body);
+
+ // allow unknown or self-signed certificates
+ if (!LanguageToolCfg::SSLCertVerify::get())
+ {
+ (void)curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, false);
+ (void)curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, false);
+ }
+
+ if (method == HTTP_METHOD::HTTP_POST)
+ {
+ (void)curl_easy_setopt(curl.get(), CURLOPT_POST, 1L);
+ (void)curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, aPostData.getStr());
+ }
+
+ CURLcode cc = curl_easy_perform(curl.get());
+ if (cc != CURLE_OK)
+ {
+ SAL_WARN("languagetool",
+ "CURL request returned with error: " << static_cast<sal_Int32>(cc));
+ }
+
+ curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &nStatusCode);
+ return response_body;
+}
+
+std::string makeDudenHttpRequest(std::u16string_view aURL, const OString& aPostData,
+ tools::Long& nStatusCode)
+{
+ struct curl_slist* pList = nullptr;
+ OString sAccessToken
+ = OUStringToOString(LanguageToolCfg::ApiKey::get().value_or(""), RTL_TEXTENCODING_UTF8);
+
+ pList = curl_slist_append(pList, "Cache-Control: no-cache");
+ pList = curl_slist_append(pList, "Content-Type: application/json");
+ if (!sAccessToken.isEmpty())
+ {
+ sAccessToken = "access_token: " + sAccessToken;
+ pList = curl_slist_append(pList, sAccessToken.getStr());
+ }
+
+ return makeHttpRequest_impl(aURL, HTTP_METHOD::HTTP_POST, aPostData, pList, nStatusCode);
+}
+
+std::string makeHttpRequest(std::u16string_view aURL, HTTP_METHOD method, const OString& aPostData,
+ tools::Long& nStatusCode)
+{
+ OString realPostData(aPostData);
+ if (method == HTTP_METHOD::HTTP_POST)
+ {
+ OString apiKey
+ = OUStringToOString(LanguageToolCfg::ApiKey::get().value_or(""), RTL_TEXTENCODING_UTF8);
+ OUString username = LanguageToolCfg::Username::get().value_or("");
+ if (!apiKey.isEmpty() && !username.isEmpty())
+ realPostData += "&username=" + encodeTextForLT(username) + "&apiKey=" + apiKey;
+ }
+
+ return makeHttpRequest_impl(aURL, method, realPostData, nullptr, nStatusCode);
+}
+
+template <typename Func>
+uno::Sequence<SingleProofreadingError> parseJson(std::string&& json, std::string path, Func f)
+{
+ std::stringstream aStream(std::move(json)); // Optimized in C++20
+ boost::property_tree::ptree aRoot;
+ boost::property_tree::read_json(aStream, aRoot);
+
+ if (auto tree = aRoot.get_child_optional(path))
+ {
+ uno::Sequence<SingleProofreadingError> aErrors(tree->size());
+ auto it = tree->begin();
+ for (auto& rError : asNonConstRange(aErrors))
+ {
+ f(it->second, rError);
+ it++;
+ }
+ return aErrors;
+ }
+ return {};
+}
+
+void parseDudenResponse(ProofreadingResult& rResult, std::string&& aJSONBody)
+{
+ rResult.aErrors = parseJson(
+ std::move(aJSONBody), "check-positions",
+ [](const boost::property_tree::ptree& rPos, SingleProofreadingError& rError) {
+ rError.nErrorStart = rPos.get<int>("offset", 0);
+ rError.nErrorLength = rPos.get<int>("length", 0);
+ rError.nErrorType = text::TextMarkupType::PROOFREADING;
+ //rError.aShortComment = ??
+ //rError.aFullComment = ??
+ const std::string sType = rPos.get<std::string>("type", {});
+ rError.aProperties = { lcl_GetLineColorPropertyFromErrorId(sType) };
+
+ const auto proposals = rPos.get_child_optional("proposals");
+ if (!proposals)
+ return;
+ rError.aSuggestions.realloc(std::min(proposals->size(), MAX_SUGGESTIONS_SIZE));
+ auto itProp = proposals->begin();
+ for (auto& rSuggestion : asNonConstRange(rError.aSuggestions))
+ {
+ rSuggestion = OStringToOUString(itProp->second.data(), RTL_TEXTENCODING_UTF8);
+ itProp++;
+ }
+ });
+}
+
+/*
+ rResult is both input and output
+ aJSONBody is the response body from the HTTP Request to LanguageTool API
+*/
+void parseProofreadingJSONResponse(ProofreadingResult& rResult, std::string&& aJSONBody)
+{
+ rResult.aErrors = parseJson(
+ std::move(aJSONBody), "matches",
+ [](const boost::property_tree::ptree& match, SingleProofreadingError& rError) {
+ rError.nErrorStart = match.get<int>("offset", 0);
+ rError.nErrorLength = match.get<int>("length", 0);
+ rError.nErrorType = text::TextMarkupType::PROOFREADING;
+ const std::string shortMessage = match.get<std::string>("message", {});
+ const std::string message = match.get<std::string>("shortMessage", {});
+
+ rError.aShortComment = OStringToOUString(shortMessage, RTL_TEXTENCODING_UTF8);
+ rError.aFullComment = OStringToOUString(message, RTL_TEXTENCODING_UTF8);
+
+ // Parse the error category for Line Color
+ std::string errorCategoryId;
+ if (auto rule = match.get_child_optional("rule"))
+ if (auto ruleCategory = rule->get_child_optional("category"))
+ errorCategoryId = ruleCategory->get<std::string>("id", {});
+ rError.aProperties = { lcl_GetLineColorPropertyFromErrorId(errorCategoryId) };
+
+ const auto replacements = match.get_child_optional("replacements");
+ if (!replacements)
+ return;
+ // 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.)
+ rError.aSuggestions.realloc(std::min(replacements->size(), MAX_SUGGESTIONS_SIZE));
+ auto itRep = replacements->begin();
+ for (auto& rSuggestion : asNonConstRange(rError.aSuggestions))
+ {
+ std::string replacementStr = itRep->second.get<std::string>("value", {});
+ rSuggestion = OStringToOUString(replacementStr, RTL_TEXTENCODING_UTF8);
+ itRep++;
+ }
+ });
+}
+
+OUString getCheckerURL()
+{
+ if (auto oURL = LanguageToolCfg::BaseURL::get())
+ if (!oURL->isEmpty())
+ return *oURL + "/check";
+ return {};
+}
+}
+
+LanguageToolGrammarChecker::LanguageToolGrammarChecker()
+ : mCachedResults(10)
+{
+}
+
+LanguageToolGrammarChecker::~LanguageToolGrammarChecker() {}
+
+sal_Bool SAL_CALL LanguageToolGrammarChecker::isSpellChecker() { return false; }
+
+sal_Bool SAL_CALL LanguageToolGrammarChecker::hasLocale(const Locale& rLocale)
+{
+ if (!m_aSuppLocales.hasElements())
+ getLocales();
+
+ for (auto const& suppLocale : std::as_const(m_aSuppLocales))
+ if (rLocale == suppLocale)
+ return true;
+
+ SAL_INFO("languagetool", "No locale \"" << LanguageTag::convertToBcp47(rLocale, false) << "\"");
+ return false;
+}
+
+uno::Sequence<Locale> SAL_CALL LanguageToolGrammarChecker::getLocales()
+{
+ osl::MutexGuard aGuard(linguistic::GetLinguMutex());
+
+ if (m_aSuppLocales.hasElements())
+ return m_aSuppLocales;
+
+ if (!LanguageToolCfg::IsEnabled::get())
+ return m_aSuppLocales;
+
+ SvtLinguConfig aLinguCfg;
+ uno::Sequence<OUString> aLocaleList;
+
+ if (LanguageToolCfg::RestProtocol::get().value_or("") == sDuden)
+ {
+ aLocaleList.realloc(3);
+ aLocaleList.getArray()[0] = "de-DE";
+ aLocaleList.getArray()[1] = "en-US";
+ aLocaleList.getArray()[2] = "en-GB";
+ }
+ else
+ aLinguCfg.GetLocaleListFor("GrammarCheckers",
+ "org.openoffice.lingu.LanguageToolGrammarChecker", aLocaleList);
+
+ auto nLength = aLocaleList.getLength();
+ m_aSuppLocales.realloc(nLength);
+ auto pArray = m_aSuppLocales.getArray();
+ auto pLocaleList = aLocaleList.getArray();
+
+ for (auto i = 0; i < nLength; i++)
+ {
+ pArray[i] = LanguageTag::convertToLocale(pLocaleList[i]);
+ }
+
+ return m_aSuppLocales;
+}
+
+ProofreadingResult SAL_CALL LanguageToolGrammarChecker::doProofreading(
+ const OUString& aDocumentIdentifier, const OUString& aText, const Locale& aLocale,
+ sal_Int32 nStartOfSentencePosition, sal_Int32 nSuggestedBehindEndOfSentencePosition,
+ const uno::Sequence<PropertyValue>& 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 = {};
+ xRes.xProofreader = this;
+ xRes.aErrors = {};
+
+ if (aText.isEmpty())
+ {
+ return xRes;
+ }
+
+ if (nStartOfSentencePosition != 0)
+ {
+ return xRes;
+ }
+
+ xRes.nStartOfNextSentencePosition = aText.getLength();
+
+ if (!LanguageToolCfg::IsEnabled::get())
+ {
+ return xRes;
+ }
+
+ OUString checkerURL = getCheckerURL();
+ 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());
+
+ OString langTag(LanguageTag::convertToBcp47(aLocale, false).toUtf8());
+ OString postData;
+ const bool bDudenProtocol = LanguageToolCfg::RestProtocol::get().value_or("") == "duden";
+ if (bDudenProtocol)
+ {
+ std::stringstream aStream;
+ boost::property_tree::ptree aTree;
+ aTree.put("text-language", langTag.getStr());
+ aTree.put("text", aText.toUtf8()); // We don't encode the text in Duden Corrector tool case.
+ aTree.put("hyphenation", false);
+ aTree.put("spellchecking-level", 3);
+ aTree.put("correction-proposals", true);
+ boost::property_tree::write_json(aStream, aTree);
+ postData = OString(aStream.str());
+ }
+ else
+ {
+ postData = "text=" + encodeTextForLT(aText) + "&language=" + langTag;
+ }
+
+ if (auto cachedResult = mCachedResults.find(postData); cachedResult != mCachedResults.end())
+ {
+ xRes.aErrors = cachedResult->second;
+ return xRes;
+ }
+
+ tools::Long http_code = 0;
+ std::string response_body;
+ if (bDudenProtocol)
+ response_body = makeDudenHttpRequest(checkerURL, postData, http_code);
+ else
+ response_body = makeHttpRequest(checkerURL, HTTP_METHOD::HTTP_POST, postData, http_code);
+
+ if (http_code != 200)
+ {
+ return xRes;
+ }
+
+ if (response_body.length() <= 0)
+ {
+ return xRes;
+ }
+
+ if (bDudenProtocol)
+ {
+ parseDudenResponse(xRes, std::move(response_body));
+ }
+ else
+ {
+ parseProofreadingJSONResponse(xRes, std::move(response_body));
+ }
+ // cache the result
+ mCachedResults.insert(std::make_pair(postData, xRes.aErrors));
+ return xRes;
+}
+
+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);
+}
+
+uno::Sequence<OUString> SAL_CALL LanguageToolGrammarChecker::getSupportedServiceNames()
+{
+ return { SN_GRAMMARCHECKER };
+}
+
+void SAL_CALL LanguageToolGrammarChecker::initialize(const uno::Sequence<uno::Any>&) {}
+
+extern "C" SAL_DLLPUBLIC_EXPORT css::uno::XInterface*
+lingucomponent_LanguageToolGrammarChecker_get_implementation(
+ css::uno::XComponentContext*, css::uno::Sequence<css::uno::Any> const&)
+{
+ return cppu::acquire(new LanguageToolGrammarChecker());
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
diff --git a/lingucomponent/source/spellcheck/languagetool/languagetoolimp.hxx b/lingucomponent/source/spellcheck/languagetool/languagetoolimp.hxx
new file mode 100644
index 0000000000..93d2c84c61
--- /dev/null
+++ b/lingucomponent/source/spellcheck/languagetool/languagetoolimp.hxx
@@ -0,0 +1,75 @@
+/* -*- 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 <com/sun/star/lang/XInitialization.hpp>
+#include <com/sun/star/lang/XServiceDisplayName.hpp>
+#include <com/sun/star/lang/XServiceInfo.hpp>
+#include <com/sun/star/lang/XServiceName.hpp>
+#include <com/sun/star/linguistic2/XProofreader.hpp>
+#include <com/sun/star/linguistic2/ProofreadingResult.hpp>
+#include <com/sun/star/beans/XPropertySet.hpp>
+#include <com/sun/star/beans/PropertyValues.hpp>
+#include <linguistic/misc.hxx>
+#include <string_view>
+#include <o3tl/lru_map.hxx>
+#include <tools/long.hxx>
+
+class LanguageToolGrammarChecker
+ : public cppu::WeakImplHelper<css::linguistic2::XProofreader, css::lang::XInitialization,
+ css::lang::XServiceInfo, css::lang::XServiceDisplayName>
+{
+ css::uno::Sequence<css::lang::Locale> m_aSuppLocales;
+ o3tl::lru_map<OString, css::uno::Sequence<css::linguistic2::SingleProofreadingError>>
+ mCachedResults;
+ LanguageToolGrammarChecker(const LanguageToolGrammarChecker&) = delete;
+ LanguageToolGrammarChecker& operator=(const LanguageToolGrammarChecker&) = delete;
+
+public:
+ LanguageToolGrammarChecker();
+ virtual ~LanguageToolGrammarChecker() override;
+
+ // XSupportedLocales
+ virtual css::uno::Sequence<css::lang::Locale> SAL_CALL getLocales() override;
+ virtual sal_Bool SAL_CALL hasLocale(const css::lang::Locale& rLocale) override;
+
+ // XProofReader
+ virtual sal_Bool SAL_CALL isSpellChecker() override;
+ virtual css::linguistic2::ProofreadingResult SAL_CALL
+ doProofreading(const OUString& aDocumentIdentifier, const OUString& aText,
+ const css::lang::Locale& aLocale, sal_Int32 nStartOfSentencePosition,
+ sal_Int32 nSuggestedBehindEndOfSentencePosition,
+ const css::uno::Sequence<css::beans::PropertyValue>& aProperties) override;
+
+ virtual void SAL_CALL ignoreRule(const OUString& aRuleIdentifier,
+ const css::lang::Locale& aLocale) override;
+ virtual void SAL_CALL resetIgnoreRules() override;
+
+ // XServiceDisplayName
+ virtual OUString SAL_CALL getServiceDisplayName(const css::lang::Locale& rLocale) override;
+
+ // XInitialization
+ virtual void SAL_CALL initialize(const css::uno::Sequence<css::uno::Any>& rArguments) override;
+
+ // XServiceInfo
+ virtual OUString SAL_CALL getImplementationName() override;
+ virtual sal_Bool SAL_CALL supportsService(const OUString& rServiceName) override;
+ virtual css::uno::Sequence<OUString> SAL_CALL getSupportedServiceNames() override;
+};
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
diff --git a/lingucomponent/source/spellcheck/macosxspell/MacOSXSpell.component b/lingucomponent/source/spellcheck/macosxspell/MacOSXSpell.component
new file mode 100644
index 0000000000..b1fe7d612a
--- /dev/null
+++ b/lingucomponent/source/spellcheck/macosxspell/MacOSXSpell.component
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * 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 .
+ -->
+
+<component loader="com.sun.star.loader.SharedLibrary" environment="@CPPU_ENV@"
+ xmlns="http://openoffice.org/2010/uno-components">
+ <implementation name="org.openoffice.lingu.MacOSXSpellChecker"
+ constructor="lingucomponent_MacSpellChecker_get_implementation" single-instance="true">
+ <service name="com.sun.star.linguistic2.SpellChecker"/>
+ </implementation>
+</component>
diff --git a/lingucomponent/source/spellcheck/macosxspell/macspellimp.hxx b/lingucomponent/source/spellcheck/macosxspell/macspellimp.hxx
new file mode 100644
index 0000000000..776c474d21
--- /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 <comphelper/interfacecontainer3.hxx>
+#include <cppuhelper/implbase.hxx>
+
+#include <premac.h>
+#ifdef MACOSX
+#import <Cocoa/Cocoa.h>
+#else
+#include <UIKit/UIKit.h>
+#endif
+#include <postmac.h>
+#include <com/sun/star/lang/XComponent.hpp>
+#include <com/sun/star/lang/XInitialization.hpp>
+#include <com/sun/star/lang/XServiceDisplayName.hpp>
+#include <com/sun/star/beans/XPropertySet.hpp>
+#include <com/sun/star/lang/XServiceInfo.hpp>
+#include <com/sun/star/linguistic2/XSpellChecker.hpp>
+#include <com/sun/star/linguistic2/XLinguServiceEventBroadcaster.hpp>
+
+#include <linguistic/misc.hxx>
+#include <linguistic/lngprophelp.hxx>
+
+#include <lingutil.hxx>
+
+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<XEventListener> 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<PropertyValue>& rProperties ) override;
+ virtual Reference< XSpellAlternatives > SAL_CALL spell( const OUString& rWord, const Locale& rLocale, const css::uno::Sequence<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: */
diff --git a/lingucomponent/source/spellcheck/macosxspell/macspellimp.mm b/lingucomponent/source/spellcheck/macosxspell/macspellimp.mm
new file mode 100644
index 0000000000..448870e912
--- /dev/null
+++ b/lingucomponent/source/spellcheck/macosxspell/macspellimp.mm
@@ -0,0 +1,678 @@
+/* -*- 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 <com/sun/star/uno/Reference.h>
+
+#include <com/sun/star/linguistic2/SpellFailure.hpp>
+#include <com/sun/star/linguistic2/XLinguProperties.hpp>
+#include <cppuhelper/factory.hxx>
+#include <cppuhelper/supportsservice.hxx>
+#include <cppuhelper/weak.hxx>
+#include <com/sun/star/registry/XRegistryKey.hpp>
+#include <com/sun/star/lang/XSingleServiceFactory.hpp>
+#include <tools/debug.hxx>
+#include <osl/mutex.hxx>
+
+#include "macspellimp.hxx"
+
+#include <linguistic/spelldta.hxx>
+#include <unotools/pathoptions.hxx>
+#include <unotools/useroptions.hxx>
+#include <osl/file.hxx>
+#include <rtl/ref.hxx>
+#include <rtl/ustrbuf.hxx>
+
+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<XSpellChecker *>(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<NSString *> 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<NSString*>([aSpellCheckLanguages objectAtIndex:i]);
+
+ // Fix up generic languages (without territory code) and odd combinations that LO
+ // doesn't handle.
+ if ([pLangStr isEqualToString:@"ar"])
+ {
+ const std::vector<NSString*> aAR
+ { @"AE", @"BH", @"DJ", @"DZ", @"EG", @"ER", @"IL", @"IQ", @"JO",
+ @"KM", @"KW", @"LB", @"LY", @"MA", @"MR", @"OM", @"PS", @"QA",
+ @"SA", @"SD", @"SO", @"SY", @"TD", @"TN", @"YE" };
+ for (auto c: aAR)
+ {
+ pLangStr = [@"ar_" stringByAppendingString: c];
+ postspdict.push_back( pLangStr );
+ }
+ }
+ else 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<NSString*> 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<NSString*> 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<NSString*> 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<NSString*> 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<NSString*> 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<NSString*> 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<NSString*> 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<unichar const *>(nWord.getStr()) length: nWord.getLength()]autorelease];
+ NSString* aLang = [[[NSString alloc] initWithCharacters: reinterpret_cast<unichar const *>(rLocale.Language.getStr()) length: rLocale.Language.getLength()]autorelease];
+ if(rLocale.Country.getLength()>0)
+ {
+ NSString* aCountry = [[[NSString alloc] initWithCharacters: reinterpret_cast<unichar const *>(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<PropertyValue>& 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<unichar const *>(nWord.getStr()) length: nWord.getLength()]autorelease];
+ NSString* aLang = [[[NSString alloc] initWithCharacters: reinterpret_cast<unichar const *>(rLocale.Language.getStr()) length: rLocale.Language.getLength()]autorelease];
+ if(rLocale.Country.getLength()>0)
+ {
+ NSString* aCountry = [[[NSString alloc] initWithCharacters: reinterpret_cast<unichar const *>(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<const sal_Unicode*>([guess cStringUsingEncoding:NSUnicodeStringEncoding]), static_cast<sal_Int32>([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<SpellAlternatives> 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<PropertyValue>& 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<XSpellChecker *>(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<XSpellChecker *>(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<css::uno::Any> 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 0000000000..c284e13fc3
--- /dev/null
+++ b/lingucomponent/source/spellcheck/spell/spell.component
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * 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 .
+ -->
+
+<component loader="com.sun.star.loader.SharedLibrary" environment="@CPPU_ENV@"
+ xmlns="http://openoffice.org/2010/uno-components">
+ <implementation name="org.openoffice.lingu.MySpellSpellChecker"
+ constructor="lingucomponent_SpellChecker_get_implementation" single-instance="true">
+ <service name="com.sun.star.linguistic2.SpellChecker"/>
+ </implementation>
+</component>
diff --git a/lingucomponent/source/spellcheck/spell/sspellimp.cxx b/lingucomponent/source/spellcheck/spell/sspellimp.cxx
new file mode 100644
index 0000000000..193ddb2c32
--- /dev/null
+++ b/lingucomponent/source/spellcheck/spell/sspellimp.cxx
@@ -0,0 +1,648 @@
+/* -*- 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 <com/sun/star/uno/Reference.h>
+
+#include <com/sun/star/linguistic2/SpellFailure.hpp>
+#include <com/sun/star/linguistic2/XLinguProperties.hpp>
+#include <comphelper/lok.hxx>
+#include <comphelper/processfactory.hxx>
+#include <cppuhelper/supportsservice.hxx>
+#include <cppuhelper/weak.hxx>
+#include <com/sun/star/lang/XMultiServiceFactory.hpp>
+#include <tools/debug.hxx>
+#include <osl/mutex.hxx>
+#include <osl/thread.h>
+#include <com/sun/star/ucb/XSimpleFileAccess.hpp>
+
+#include <lingutil.hxx>
+#include <hunspell.hxx>
+#include "sspellimp.hxx"
+
+#include <linguistic/misc.hxx>
+#include <linguistic/spelldta.hxx>
+#include <i18nlangtag/languagetag.hxx>
+#include <svtools/strings.hrc>
+#include <unotools/lingucfg.hxx>
+#include <unotools/resmgr.hxx>
+#include <osl/file.hxx>
+#include <rtl/ustrbuf.hxx>
+#include <rtl/textenc.h>
+#include <sal/log.hxx>
+
+#include <numeric>
+#include <utility>
+#include <vector>
+#include <set>
+#include <string.h>
+
+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"<?xml?>";
+#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<XSpellChecker *>(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<OUString> 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, int& rInfo)
+{
+ 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<Hunspell>(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), &rInfo);
+#else
+ bool bVal = pMS->spell(aWrd.getStr(), &rInfo) != 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), &rInfo);
+#else
+ bVal = pMS->spell(bWrd.getStr(), &rInfo) != 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 );
+
+ int nInfo = 0;
+ sal_Int16 nFailure = GetSpellFailure( rWord, rLocale, nInfo );
+ 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;
+ }
+//#define SPELL_COMPOUND 1 << 0
+
+ // valid word, but it's a rule-based compound word
+ if ( nFailure == -1 && (nInfo & SPELL_COMPOUND) )
+ {
+ bool bHasHyphen = rWord.indexOf('-') > -1;
+ if ( (bHasHyphen && !rHelper.IsSpellHyphenatedCompound()) ||
+ (!bHasHyphen && !rHelper.IsSpellClosedCompound()) )
+ {
+ return false;
+ }
+ }
+
+ 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<std::string> suglst = pMS->suggest(std::string(aWrd));
+ 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<XSpellChecker *>(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<XSpellChecker *>(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<css::uno::Any> 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 0000000000..68ddc69b3c
--- /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 <comphelper/interfacecontainer3.hxx>
+#include <cppuhelper/implbase.hxx>
+#include <com/sun/star/lang/XComponent.hpp>
+#include <com/sun/star/lang/XInitialization.hpp>
+#include <com/sun/star/lang/XServiceDisplayName.hpp>
+#include <com/sun/star/beans/PropertyValue.hpp>
+#include <com/sun/star/lang/XServiceInfo.hpp>
+#include <com/sun/star/linguistic2/XSpellChecker.hpp>
+#include <com/sun/star/linguistic2/XLinguServiceEventBroadcaster.hpp>
+
+#include <linguistic/lngprophelp.hxx>
+
+#include <memory>
+
+#include <hunspell.hxx>
+
+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<Hunspell> m_pDict;
+ rtl_TextEncoding m_aDEnc;
+
+ DictItem(OUString i_DName, Locale i_DLoc, rtl_TextEncoding i_DEnc);
+ };
+
+ std::vector<DictItem> m_DictItems;
+
+ Sequence< Locale > m_aSuppLocales;
+
+ ::comphelper::OInterfaceContainerHelper3<XEventListener> m_aEvtListeners;
+ std::unique_ptr<linguistic::PropertyHelper_Spelling> 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, int& rInfo );
+ 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: */