From 7fb29aed802f8b140cd81712f80772243548a5cf Mon Sep 17 00:00:00 2001 From: Simon Rozman Date: Tue, 21 Dec 2021 14:21:28 +0100 Subject: [PATCH] Introduce bad ZRCola Unicode Composition highlighting Signed-off-by: Simon Rozman --- ZRCola/locale/ZRCola.pot | 23 +- ZRCola/locale/de_DE.po | 8 +- ZRCola/locale/ru_RU.po | 8 +- ZRCola/locale/sl_SI.po | 8 +- ZRCola/zrcolaapp.cpp | 6 + ZRCola/zrcolaapp.h | 2 + ZRCola/zrcolacomppnl.cpp | 29 ++- ZRCola/zrcolacomppnl.h | 3 +- ZRColaCompile/dbsource.cpp | 57 +++++ ZRColaCompile/dbsource.h | 67 ++++- ZRColaCompile/main.cpp | 103 +++++--- lib/libZRCola/build/libZRCola.vcxproj | 2 + lib/libZRCola/build/libZRCola.vcxproj.filters | 6 + lib/libZRCola/include/zrcola/highlight.h | 240 ++++++++++++++++++ lib/libZRCola/src/highlight.cpp | 77 ++++++ lib/libZRCola/src/pch.h | 1 + output/data/ZRCola.zrcdb | Bin 3803180 -> 3837850 bytes 17 files changed, 555 insertions(+), 85 deletions(-) create mode 100644 lib/libZRCola/include/zrcola/highlight.h create mode 100644 lib/libZRCola/src/highlight.cpp diff --git a/ZRCola/locale/ZRCola.pot b/ZRCola/locale/ZRCola.pot index 08fbd3e..165b5d2 100644 --- a/ZRCola/locale/ZRCola.pot +++ b/ZRCola/locale/ZRCola.pot @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: ZRCola\n" -"POT-Creation-Date: 2021-12-20 20:17+0100\n" +"POT-Creation-Date: 2021-12-21 14:16+0100\n" "PO-Revision-Date: 2019-04-01 19:38+0200\n" "Last-Translator: Simon Rozman \n" "Language-Team: Amebis, d. o. o., Kamnik \n" @@ -108,36 +108,37 @@ msgstr "Prekini sestavljanje in vrni fokus nazaj izvornemu oknu" #: res/zrcolagui.cpp:138 zrcolagui.cpp:138 #, fuzzy msgid "(De)&composition" -msgstr "Vklopi/izklopi (raz-)sestavljanje ZRCola" +msgstr "(&Raz-)sestavljanje" #: res/zrcolagui.cpp:139 zrcolagui.cpp:139 #, fuzzy msgid "&None" -msgstr "(brez)" +msgstr "&Brez" #: res/zrcolagui.cpp:139 zrcolagui.cpp:139 #, fuzzy msgid "No character (De)composition" -msgstr "Vklopi/izklopi (raz-)sestavljanje ZRCola" +msgstr "Brez (raz-)sestavljanja znakov" #: res/zrcolagui.cpp:142 zrcolagui.cpp:142 #, fuzzy msgid "&ZRCola" -msgstr "ZRCola" +msgstr "&ZRCola" #: res/zrcolagui.cpp:142 zrcolagui.cpp:142 #, fuzzy msgid "ZRCola character (De)composition" -msgstr "Vklopi/izklopi (raz-)sestavljanje ZRCola" +msgstr "(Raz-)sestavljanje znakov ZRCola" #: res/zrcolagui.cpp:145 zrcolagui.cpp:145 #, fuzzy msgid "&Unicode" -msgstr "Unicode" +msgstr "&Unicode" #: res/zrcolagui.cpp:145 zrcolagui.cpp:145 +#, fuzzy msgid "Unicode character (De)composition" -msgstr "" +msgstr "(Raz-)sestavljanje znakov Unicode" #: res/zrcolagui.cpp:151 zrcolagui.cpp:151 #, fuzzy @@ -308,7 +309,7 @@ msgstr "Pošlji razstavljeno" #: res/zrcolagui.cpp:232 zrcolagui.cpp:232 #, fuzzy msgid "No (De)composition" -msgstr "Prekini raz/sestavljanje" +msgstr "Brez (raz-)sestavljanja" #: res/zrcolagui.cpp:232 res/zrcolagui.cpp:940 res/zrcolagui.h:119 #: zrcolaapp.cpp:60 zrcolafrm.cpp:118 zrcolagui.cpp:232 zrcolagui.cpp:943 @@ -807,12 +808,12 @@ msgstr "Opozorilo" msgid "ZRCola keyboard shortcut Win+F6 could not be registered. Some functionality will not be available." msgstr "ZRColine bližnjice na tipkovnici Win+F6 ni mogoče registrirati. Nekaj funkcionalnosti ne bo na voljo." -#: zrcolafrm.cpp:508 +#: zrcolafrm.cpp:513 #, fuzzy msgid "http://zrcola.zrc-sazu.si/en/info/instructions/" msgstr "http://zrcola.zrc-sazu.si/info/instructions/" -#: zrcolafrm.cpp:533 +#: zrcolafrm.cpp:538 #, fuzzy msgid "http://zrcola.zrc-sazu.si/wp-content/uploads/2016/06/ZRCola_tipkovnica_Jun2016.pdf" msgstr "http://zrcola.zrc-sazu.si/wp-content/uploads/2016/06/ZRCola_tipkovnica_Jun2016.pdf" diff --git a/ZRCola/locale/de_DE.po b/ZRCola/locale/de_DE.po index 7397bce..3680d4b 100644 --- a/ZRCola/locale/de_DE.po +++ b/ZRCola/locale/de_DE.po @@ -4,8 +4,8 @@ msgid "" msgstr "" "Project-Id-Version: ZRCola\n" -"POT-Creation-Date: 2021-12-20 20:18+0100\n" -"PO-Revision-Date: 2021-12-20 20:18+0100\n" +"POT-Creation-Date: 2021-12-21 14:17+0100\n" +"PO-Revision-Date: 2021-12-21 14:17+0100\n" "Last-Translator: Simon Rozman \n" "Language-Team: German (Germany) (https://www.transifex.com/amebis/teams/91592/de_DE/)\n" "Language: de_DE\n" @@ -655,11 +655,11 @@ msgstr "" msgid "ZRCola keyboard shortcut Win+F6 could not be registered. Some functionality will not be available." msgstr "" -#: zrcolafrm.cpp:508 +#: zrcolafrm.cpp:513 msgid "http://zrcola.zrc-sazu.si/en/info/instructions/" msgstr "http://zrcola.zrc-sazu.si/de/info/instructions/" -#: zrcolafrm.cpp:533 +#: zrcolafrm.cpp:538 msgid "http://zrcola.zrc-sazu.si/wp-content/uploads/2016/06/ZRCola_tipkovnica_Jun2016.pdf" msgstr "http://zrcola.zrc-sazu.si/wp-content/uploads/2016/06/ZRCola_tipkovnica_Jun2016.pdf" diff --git a/ZRCola/locale/ru_RU.po b/ZRCola/locale/ru_RU.po index b0bb8ae..2ce3017 100644 --- a/ZRCola/locale/ru_RU.po +++ b/ZRCola/locale/ru_RU.po @@ -5,8 +5,8 @@ msgid "" msgstr "" "Project-Id-Version: ZRCola\n" -"POT-Creation-Date: 2021-12-20 20:18+0100\n" -"PO-Revision-Date: 2021-12-20 20:18+0100\n" +"POT-Creation-Date: 2021-12-21 14:17+0100\n" +"PO-Revision-Date: 2021-12-21 14:17+0100\n" "Last-Translator: Simon Rozman \n" "Language-Team: Russian (Russia) (https://www.transifex.com/amebis/teams/91592/ru_RU/)\n" "Language: ru_RU\n" @@ -687,11 +687,11 @@ msgstr "Предупреждение" msgid "ZRCola keyboard shortcut Win+F6 could not be registered. Some functionality will not be available." msgstr "Сочетание клавиш Win+F6 невозможно регистрировать. Некоторые функциональности не будут доступны." -#: zrcolafrm.cpp:508 +#: zrcolafrm.cpp:513 msgid "http://zrcola.zrc-sazu.si/en/info/instructions/" msgstr "http://zrcola.zrc-sazu.si/ru/info/instructions/" -#: zrcolafrm.cpp:533 +#: zrcolafrm.cpp:538 msgid "http://zrcola.zrc-sazu.si/wp-content/uploads/2016/06/ZRCola_tipkovnica_Jun2016.pdf" msgstr "http://zrcola.zrc-sazu.si/wp-content/uploads/2016/06/ZRCola_tipkovnica_Jun2016.pdf" diff --git a/ZRCola/locale/sl_SI.po b/ZRCola/locale/sl_SI.po index 08ec7fe..6d36440 100644 --- a/ZRCola/locale/sl_SI.po +++ b/ZRCola/locale/sl_SI.po @@ -4,8 +4,8 @@ msgid "" msgstr "" "Project-Id-Version: ZRCola\n" -"POT-Creation-Date: 2021-12-20 20:18+0100\n" -"PO-Revision-Date: 2021-12-20 20:20+0100\n" +"POT-Creation-Date: 2021-12-21 14:17+0100\n" +"PO-Revision-Date: 2021-12-21 14:17+0100\n" "Last-Translator: Simon Rozman \n" "Language-Team: Slovenian (Slovenia) (https://www.transifex.com/amebis/teams/91592/sl_SI/)\n" "Language: sl_SI\n" @@ -666,11 +666,11 @@ msgstr "Opozorilo" msgid "ZRCola keyboard shortcut Win+F6 could not be registered. Some functionality will not be available." msgstr "ZRColine bližnjice na tipkovnici Win+F6 ni mogoče registrirati. Nekaj funkcionalnosti ne bo na voljo." -#: zrcolafrm.cpp:508 +#: zrcolafrm.cpp:513 msgid "http://zrcola.zrc-sazu.si/en/info/instructions/" msgstr "http://zrcola.zrc-sazu.si/info/instructions/" -#: zrcolafrm.cpp:533 +#: zrcolafrm.cpp:538 msgid "http://zrcola.zrc-sazu.si/wp-content/uploads/2016/06/ZRCola_tipkovnica_Jun2016.pdf" msgstr "http://zrcola.zrc-sazu.si/wp-content/uploads/2016/06/ZRCola_tipkovnica_Jun2016.pdf" diff --git a/ZRCola/zrcolaapp.cpp b/ZRCola/zrcolaapp.cpp index 7d193f4..bc5646f 100644 --- a/ZRCola/zrcolaapp.cpp +++ b/ZRCola/zrcolaapp.cpp @@ -144,6 +144,12 @@ bool ZRColaApp::OnInit() wxFAIL_MSG(wxT("Error reading tag name data from ZRCola.zrcdb.")); m_tn_db.clear(); } + } else if (id == ZRCola::highlight_rec::id) { + dat >> ZRCola::highlight_rec(m_h_db); + if (!dat.good()) { + wxFAIL_MSG(wxT("Error reading highlight data from ZRCola.zrcdb.")); + m_h_db.clear(); + } } else stdex::idrec::ignore(dat); } diff --git a/ZRCola/zrcolaapp.h b/ZRCola/zrcolaapp.h index 66a28ed..954c573 100644 --- a/ZRCola/zrcolaapp.h +++ b/ZRCola/zrcolaapp.h @@ -21,6 +21,7 @@ class ZRColaApp; #include #pragma warning(pop) #include +#include #include #include #include @@ -72,6 +73,7 @@ public: ZRCola::chrcat_db m_cc_db; ///< Characted category database ZRCola::chrtag_db m_ct_db; ///< Character tag database ZRCola::tagname_db m_tn_db; ///< Tag name database + ZRCola::highlight_db m_h_db; ///< Highlight database wxZRColaFrame *m_mainWnd; ///< Main window diff --git a/ZRCola/zrcolacomppnl.cpp b/ZRCola/zrcolacomppnl.cpp index 8049357..11a5290 100644 --- a/ZRCola/zrcolacomppnl.cpp +++ b/ZRCola/zrcolacomppnl.cpp @@ -17,6 +17,7 @@ wxZRColaComposerPanel::wxZRColaComposerPanel(wxWindow* parent) : m_destinationRestyled(false), m_styleNormal(*wxBLACK, *wxWHITE, wxFont(20, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, wxT("ZRCola"))), m_stylePUA(*wxBLUE, *wxWHITE, wxFont(20, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, wxT("ZRCola"))), + m_styleZRColaUnicodeComposedIssues(*wxRED, *wxWHITE, wxFont(20, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, wxT("ZRCola"))), m_selSource(0, 0), m_selDestination(0, 0), wxZRColaComposerPanelBase(parent) @@ -320,17 +321,25 @@ void wxZRColaComposerPanel::OnDestinationText(wxCommandEvent& event) auto app = dynamic_cast(wxTheApp); m_destinationRestyled = true; - if (app->m_mainWnd->m_warnPUA) { - wxString src = m_destination->GetValue(); - size_t len = src.Length(); - for (size_t i = 0, j; i < len;) { - bool pua_i = ZRCola::ispua(src[i]); - for (j = i + 1; j < len && pua_i == ZRCola::ispua(src[j]); j++); - m_destination->SetStyle((long)i, (long)j, pua_i ? m_stylePUA : m_styleNormal); - i = j; + wxString src = m_destination->GetValue(); + app->m_h_db.Highlight(src, src.Length(), [this, app, src](ZRCola::hlghtsetid_t set, size_t start, size_t end) { + switch (set) { + case ZRCOLA_HLGHTSETID_ZRCOLA_UNICODE_COMPOSED_ISSUES: + m_destination->SetStyle((long)start, (long)end, m_styleZRColaUnicodeComposedIssues); + break; + + default: + if (app->m_mainWnd->m_warnPUA) { + for (size_t i = start, j; i < end;) { + bool pua_i = ZRCola::ispua(src[i]); + for (j = i + 1; j < end && pua_i == ZRCola::ispua(src[j]); j++); + m_destination->SetStyle((long)i, (long)j, pua_i ? m_stylePUA : m_styleNormal); + i = j; + } + } else + m_destination->SetStyle((long)start, (long)end, m_styleNormal); } - } else - m_destination->SetStyle(0, GetWindowTextLength(m_destination->GetHWND()), m_styleNormal); + }); m_destinationRestyled = false; } diff --git a/ZRCola/zrcolacomppnl.h b/ZRCola/zrcolacomppnl.h index 6811ae3..42d7333 100644 --- a/ZRCola/zrcolacomppnl.h +++ b/ZRCola/zrcolacomppnl.h @@ -63,7 +63,8 @@ protected: m_destinationRestyled; ///< Boolean flag to mark destination text is being restyled wxTextAttr m_styleNormal, ///< Normal text style - m_stylePUA; ///< PUA character text style + m_stylePUA, ///< PUA character text style + m_styleZRColaUnicodeComposedIssues; ///< ZRCola Unicode Composed issues character text style std::vector m_mapping; ///< Character index mapping vector between source and normalized text std::pair m_selSource, ///< Character index of selected text in source text control diff --git a/ZRColaCompile/dbsource.cpp b/ZRColaCompile/dbsource.cpp index c67a37e..361abb0 100644 --- a/ZRColaCompile/dbsource.cpp +++ b/ZRColaCompile/dbsource.cpp @@ -242,6 +242,8 @@ ZRCola::DBSource::~DBSource() m_comTranslation.free(); m_pCharacterGroup1.free(); m_comCharacterGroup.free(); + m_pHighlight1.free(); + m_comHighlight.free(); if (m_db) m_db->Close(); @@ -326,6 +328,23 @@ bool ZRCola::DBSource::Open(LPCTSTR filename) wxVERIFY(SUCCEEDED(params->Append(m_pTranslationSets1))); } + wxASSERT_MSG(!m_comHighlight, wxT("ADO command already created")); + wxVERIFY(SUCCEEDED(::CoCreateInstance(CLSID_CADOCommand, NULL, CLSCTX_ALL, IID_IADOCommand, (LPVOID*)&m_comHighlight))); + wxVERIFY(SUCCEEDED(m_comHighlight->put_ActiveConnection(variant(m_db)))); + wxVERIFY(SUCCEEDED(m_comHighlight->put_CommandType(adCmdText))); + wxVERIFY(SUCCEEDED(m_comHighlight->put_CommandText(bstr(L"SELECT [komb] " + L"FROM [VRS_HighlightChars2] " + L"WHERE [group]=? " + L"ORDER BY [komb]")))); + { + // Create and add command parameters. + com_obj params; + wxVERIFY(SUCCEEDED(m_comHighlight->get_Parameters(¶ms))); + wxASSERT_MSG(!m_pHighlight1, wxT("ADO command parameter already created")); + wxVERIFY(SUCCEEDED(m_comHighlight->CreateParameter(bstr(L"@group"), adSmallInt, adParamInput, 0, variant(DISP_E_PARAMNOTFOUND, VT_ERROR), &m_pHighlight1))); + wxVERIFY(SUCCEEDED(params->Append(m_pHighlight1))); + } + return true; } else { _ftprintf(stderr, wxT("%s: error ZCC0011: Could not open database (0x%x).\n"), (LPCTSTR)filename, hr); @@ -1439,3 +1458,41 @@ bool ZRCola::DBSource::GetTagName(const winstd::com_obj& rs, tagna return true; } + + +bool ZRCola::DBSource::SelectHighlights(short set, winstd::com_obj& rs) const +{ + // Create a new recordset. + rs.free(); + wxVERIFY(SUCCEEDED(::CoCreateInstance(CLSID_CADORecordset, NULL, CLSCTX_ALL, IID_IADORecordset, (LPVOID*)&rs))); + wxVERIFY(SUCCEEDED(rs->put_CursorLocation(adUseClient))); + wxVERIFY(SUCCEEDED(rs->put_CursorType(adOpenForwardOnly))); + wxVERIFY(SUCCEEDED(rs->put_LockType(adLockReadOnly))); + + // Open it. + wxVERIFY(SUCCEEDED(m_pHighlight1->put_Value(variant(set)))); + if (FAILED(rs->Open(variant(m_comHighlight), variant(DISP_E_PARAMNOTFOUND, VT_ERROR)))) { + _ftprintf(stderr, wxT("%s: error ZCC0101: Error loading highlights from database. Please make sure the file is ZRCola.zrc compatible.\n"), m_filename.c_str()); + LogErrors(); + return false; + } + + return true; +} + + +bool ZRCola::DBSource::GetHighlight(const com_obj& rs, ZRCola::DBSource::highlight& h) const +{ + wxASSERT_MSG(rs, wxT("recordset is empty")); + + com_obj flds; + wxVERIFY(SUCCEEDED(rs->get_Fields(&flds))); + + { + com_obj f; + wxVERIFY(SUCCEEDED(flds->get_Item(variant(L"komb"), &f))); + wxCHECK(GetUnicodeString(f, h.chr), false); + } + + return true; +} diff --git a/ZRColaCompile/dbsource.h b/ZRColaCompile/dbsource.h index 80c490d..d176c59 100644 --- a/ZRColaCompile/dbsource.h +++ b/ZRColaCompile/dbsource.h @@ -6,7 +6,7 @@ #pragma once #include -#include +#include #include #include #include @@ -404,6 +404,18 @@ namespace ZRCola { }; + /// + /// Highlight + /// + class highlight { + public: + short set; ///< Highlight set ID + std::wstring chr; ///< Character sequence + + inline highlight() : set((short)ZRCOLA_HLGHTSETID_DEFAULT) {} + }; + + public: DBSource(); virtual ~DBSource(); @@ -452,18 +464,6 @@ namespace ZRCola { return SUCCEEDED(rs->get_RecordCount(&count)) ? count : (size_t)-1; } - /// - /// Splits string to individual keywords - /// - /// \param[in ] str String - /// \param[out] keywords Array of keywords - /// - /// \returns - /// - true when successful - /// - false otherwise - /// - static bool GetKeywords(const wchar_t *str, std::vector< std::wstring > &keywords); - /// /// Gets boolean from ZRCola.zrc database /// @@ -872,6 +872,30 @@ namespace ZRCola { /// bool GetTagName(const winstd::com_obj& rs, tagname& tn) const; + /// + /// Returns character highlights by set + /// + /// \param[in ] set Highlight set ID + /// \param[out] rs Recordset with results + /// + /// \returns + /// - true when query succeeds + /// - false otherwise + /// + bool SelectHighlights(short set, winstd::com_obj& rs) const; + + /// + /// Returns highlight data + /// + /// \param[in] rs Recordset with results + /// \param[out] h Highlight + /// + /// \returns + /// - true when succeeded + /// - false otherwise + /// + bool GetHighlight(const winstd::com_obj& rs, highlight& h) const; + protected: std::basic_string m_filename; ///< Database filename winstd::com_obj m_db; ///< Database @@ -886,6 +910,9 @@ namespace ZRCola { winstd::com_obj m_comTranslationSets; ///< ADO Command for GetTranslationSeq subquery winstd::com_obj m_pTranslationSets1; ///< \c m_comTranslationSets parameter + winstd::com_obj m_comHighlight; ///< ADO Command for SelectHighlights subquery + winstd::com_obj m_pHighlight1; ///< \c m_comHighlights parameter + std::set m_terms_ignore; ///< Terms to ignore when comparing characters }; }; @@ -1098,3 +1125,17 @@ inline ZRCola::tagname_db& operator<<(_Inout_ ZRCola::tagname_db &db, _In_ const return db; } + + +inline ZRCola::highlight_db& operator<<(_Inout_ ZRCola::highlight_db &db, _In_ const ZRCola::DBSource::highlight &rec) +{ + unsigned __int32 idx = db.data.size(); + db.data.push_back((unsigned __int16)rec.set); + std::wstring::size_type n = rec.chr.length(); + wxASSERT_MSG(n <= 0xffff, wxT("character overflow")); + db.data.push_back((unsigned __int16)n); + db.data.insert(db.data.end(), rec.chr.cbegin(), rec.chr.cend()); + db.idxChr.push_back(idx); + + return db; +} diff --git a/ZRColaCompile/main.cpp b/ZRColaCompile/main.cpp index 7cd3d8b..960e2a7 100644 --- a/ZRColaCompile/main.cpp +++ b/ZRColaCompile/main.cpp @@ -374,9 +374,9 @@ int _tmain(int argc, _TCHAR *argv[]) } // Preallocate memory. - db_trans.idxSrc.reserve(count); - db_trans.idxDst.reserve(count); - db_trans.data .reserve(count*5); + db_trans.idxSrc.reserve(count*2); + db_trans.idxDst.reserve(count*2); + db_trans.data .reserve(count*2*8); // Parse translations and build index and data. ZRCola::DBSource::translation trans; @@ -399,6 +399,7 @@ int _tmain(int argc, _TCHAR *argv[]) } if (!has_pua) { trans.set = (short)ZRCOLA_TRANSETID_UNICODE; + trans.dst.rank += 50; db_trans << trans; } } @@ -416,7 +417,13 @@ int _tmain(int argc, _TCHAR *argv[]) { com_obj rs_tran; if (src.SelectTranslations(static_cast(ZRCOLA_TRANSETID_UNICODE), rs_tran)) { - if (src.GetRecordsetCount(rs_tran) < 0xffffffff) { // 4G check (-1 is reserved for error condition) + size_t count = src.GetRecordsetCount(rs_tran); + if (count < 0xffffffff) { // 4G check (-1 is reserved for error condition) + // Preallocate memory. + db_trans.idxSrc.reserve(db_trans.idxSrc.size() + count); + db_trans.idxDst.reserve(db_trans.idxDst.size() + count); + db_trans.data .reserve(db_trans.data.size() + count*8); + // Parse translations and build temporary database. ZRCola::DBSource::translation trans; trans.set = (short)ZRCOLA_TRANSETID_UNICODE; @@ -467,7 +474,13 @@ int _tmain(int argc, _TCHAR *argv[]) // Get translations. com_obj rs_tran; if (src.SelectTranslations(ts.set, rs_tran)) { - if (src.GetRecordsetCount(rs_tran) < 0xffffffff) { // 4G check (-1 is reserved for error condition) + count = src.GetRecordsetCount(rs_tran); + if (count < 0xffffffff) { // 4G check (-1 is reserved for error condition) + // Preallocate memory. + db_trans.idxSrc.reserve(db_trans.idxSrc.size() + count); + db_trans.idxDst.reserve(db_trans.idxDst.size() + count); + db_trans.data .reserve(db_trans.data.size() + count*8); + // Parse translations and build temporary database. ZRCola::DBSource::translation trans; trans.set = ts.set; @@ -500,17 +513,13 @@ int _tmain(int argc, _TCHAR *argv[]) } } - // Sort indices. - db_transset.idxTranSet.sort(); - // Write translation sets to file. + db_transset.idxTranSet.sort(); dst << ZRCola::transet_rec(db_transset); - // Sort indices. + // Write translations to file. db_trans.idxSrc.sort(); db_trans.idxDst.sort(); - - // Write translations to file. dst << ZRCola::translation_rec(db_trans); { @@ -540,11 +549,9 @@ int _tmain(int argc, _TCHAR *argv[]) has_errors = true; } - // Sort indices. + // Write translation sequences to file. db.idxTranSeq.sort(); db.idxRank .sort(); - - // Write translation sequences to file. dst << ZRCola::transeq_rec(db); } else { _ftprintf(stderr, wxT("%s: error ZCC0025: Error getting translation sequence count from database or too many translation sequences.\n"), (LPCTSTR)filenameIn.c_str()); @@ -639,10 +646,8 @@ int _tmain(int argc, _TCHAR *argv[]) has_errors = true; } - // Sort indices. - db.idxLang.sort(); - // Write languages to file. + db.idxLang.sort(); dst << ZRCola::language_rec(db); } else { _ftprintf(stderr, wxT("%s: error ZCC0009: Error getting language count from database or too many languages.\n"), (LPCTSTR)filenameIn.c_str()); @@ -680,13 +685,11 @@ int _tmain(int argc, _TCHAR *argv[]) has_errors = true; } - // Sort indices. + // Write language characters to file. db.idxChr .sort(); #ifdef ZRCOLA_LANGCHAR_LANG_IDX db.idxLang.sort(); #endif - - // Write language characters to file. dst << ZRCola::langchar_rec(db); } else { _ftprintf(stderr, wxT("%s: error ZCC0011: Error getting language characters count from database or too many langchars.\n"), (LPCTSTR)filenameIn.c_str()); @@ -729,10 +732,8 @@ int _tmain(int argc, _TCHAR *argv[]) has_errors = true; } - // Sort indices. - db.idxRank.sort(); - // Write character groups to file. + db.idxRank.sort(); dst << ZRCola::chrgrp_rec(db); } else { _ftprintf(stderr, wxT("%s: error ZCC0015: Error getting character group count from database or too many character groups.\n"), (LPCTSTR)filenameIn.c_str()); @@ -753,7 +754,6 @@ int _tmain(int argc, _TCHAR *argv[]) size_t count = src.GetRecordsetCount(rs); if (count < 0xffffffff) { // 4G check (-1 is reserved for error condition) ZRCola::DBSource::character_desc_idx idxChrDsc, idxChrDscSub; - ZRCola::DBSource::character_bank chrs; // Phase 1: Parse characters and build indexes. @@ -788,14 +788,10 @@ int _tmain(int argc, _TCHAR *argv[]) categories_used.insert(chr->second.cat); } - // Sort indices. + // Write characters to file. db.idxChr.sort(); - - // Save text indices. idxChrDsc .save(db.idxDsc ); idxChrDscSub.save(db.idxDscSub); - - // Write characters to file. dst << ZRCola::character_rec(db); } else { _ftprintf(stderr, wxT("%s: error ZCC0017: Error getting character count from database or too many characters.\n"), (LPCTSTR)filenameIn.c_str()); @@ -839,11 +835,9 @@ int _tmain(int argc, _TCHAR *argv[]) has_errors = true; } - // Sort indices. + // Write character categories to file. db.idxChrCat.sort(); db.idxRank .sort(); - - // Write character categories to file. dst << ZRCola::chrcat_rec(db); } else { _ftprintf(stderr, wxT("%s: error ZCC0019: Error getting character category count from database or too many character categories.\n"), (LPCTSTR)filenameIn.c_str()); @@ -879,11 +873,9 @@ int _tmain(int argc, _TCHAR *argv[]) has_errors = true; } - // Sort indices. + // Write characters tags to file. db.idxChr.sort(); db.idxTag.sort(); - - // Write characters tags to file. dst << ZRCola::chrtag_rec(db); } else { _ftprintf(stderr, wxT("%s: error ZCC0021: Error getting characters tags count from database or too many character tags.\n"), (LPCTSTR)filenameIn.c_str()); @@ -919,11 +911,9 @@ int _tmain(int argc, _TCHAR *argv[]) has_errors = true; } - // Sort indices. + // Write tags to file. db.idxName.sort(); db.idxTag .sort(); - - // Write tags to file. dst << ZRCola::tagname_rec(db); } else { _ftprintf(stderr, wxT("%s: error ZCC0023: Error getting tag name count from database or too many tags.\n"), (LPCTSTR)filenameIn.c_str()); @@ -935,6 +925,43 @@ int _tmain(int argc, _TCHAR *argv[]) } } + { + // Get highlights. + com_obj rs; + if (src.SelectHighlights((short)ZRCOLA_HLGHTSETID_ZRCOLA_UNICODE_COMPOSED_ISSUES, rs)) { + size_t count = src.GetRecordsetCount(rs); + if (count < 0xffffffff) { // 4G check (-1 is reserved for error condition) + ZRCola::DBSource::highlight h; + ZRCola::highlight_db db; + + // Preallocate memory. + db.idxChr.reserve(count); + db.data .reserve(count*5); + + // Parse highlights and build index and data. + h.set = (short)ZRCOLA_HLGHTSETID_ZRCOLA_UNICODE_COMPOSED_ISSUES; + for (; !ZRCola::DBSource::IsEOF(rs); rs->MoveNext()) { + // Read tag name from the database. + if (src.GetHighlight(rs, h)) { + // Add highlight to index and data. + db << h; + } else + has_errors = true; + } + + // Write highlights to file. + db.idxChr.sort(); + dst << ZRCola::highlight_rec(db); + } else { + _ftprintf(stderr, wxT("%s: error ZCC0027: Error getting highlight count from database or too many tags.\n"), (LPCTSTR)filenameIn.c_str()); + has_errors = true; + } + } else { + _ftprintf(stderr, wxT("%s: error ZCC0026: Error getting highlights from database. Please make sure the file is ZRCola.zrc compatible.\n"), (LPCTSTR)filenameIn.c_str()); + has_errors = true; + } + } + idrec::close(dst, dst_start); if (dst.fail()) { diff --git a/lib/libZRCola/build/libZRCola.vcxproj b/lib/libZRCola/build/libZRCola.vcxproj index f2d89bd..45f4cbd 100644 --- a/lib/libZRCola/build/libZRCola.vcxproj +++ b/lib/libZRCola/build/libZRCola.vcxproj @@ -64,6 +64,7 @@ + @@ -75,6 +76,7 @@ + diff --git a/lib/libZRCola/build/libZRCola.vcxproj.filters b/lib/libZRCola/build/libZRCola.vcxproj.filters index 4546b17..ea574dd 100644 --- a/lib/libZRCola/build/libZRCola.vcxproj.filters +++ b/lib/libZRCola/build/libZRCola.vcxproj.filters @@ -32,6 +32,9 @@ Source Files + + Source Files + @@ -52,5 +55,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/lib/libZRCola/include/zrcola/highlight.h b/lib/libZRCola/include/zrcola/highlight.h new file mode 100644 index 0000000..073e9ac --- /dev/null +++ b/lib/libZRCola/include/zrcola/highlight.h @@ -0,0 +1,240 @@ +/* + SPDX-License-Identifier: GPL-3.0-or-later + Copyright © 2021 Amebis +*/ + +#pragma once + +#include "common.h" + +#include + +#include + +#pragma warning(push) +#pragma warning(disable: 4200) + +/// +/// Default highlight +/// +#define ZRCOLA_HLGHTSETID_DEFAULT ((ZRCola::hlghtsetid_t)0x0000) + + +/// +/// ZRCola Unicode Composed Issues +/// +#define ZRCOLA_HLGHTSETID_ZRCOLA_UNICODE_COMPOSED_ISSUES ((ZRCola::hlghtsetid_t)0x0001) + +namespace ZRCola { + /// + /// Highlight set ID + /// + typedef unsigned __int16 hlghtsetid_t; + + /// + /// Highlight database + /// + class highlight_db { + public: +#pragma pack(push) +#pragma pack(2) + /// + /// Highlight data + /// + struct highlight { + public: + hlghtsetid_t set; ///< Highlight set ID + + protected: + unsigned __int16 chr_to; ///< Character end in \c data + wchar_t data[]; ///< Character + + private: + inline highlight(_In_ const highlight &other); + inline highlight& operator=(_In_ const highlight &other); + + public: + /// + /// Constructs the highlight + /// + /// \param[in] set Highlight set ID + /// \param[in] chr Character + /// \param[in] chr_len Number of UTF-16 characters in \p chr + /// + inline highlight( + _In_opt_ hlghtsetid_t set = 0, + _In_opt_z_count_(chr_len) const wchar_t *chr = NULL, + _In_opt_ size_t chr_len = 0) + { + this->set = set; + this->chr_to = static_cast(chr_len); + if (chr && chr_len) memcpy(this->data, chr, sizeof(wchar_t)*chr_len); + } + + inline const wchar_t* chr () const { return data; }; + inline wchar_t* chr () { return data; }; + inline const wchar_t* chr_end() const { return data + chr_to; }; + inline wchar_t* chr_end() { return data + chr_to; }; + inline unsigned __int16 chr_len() const { return chr_to; }; + + inline wchar_t chr_at(_In_ size_t i) const + { + return i < chr_to ? data[i] : 0; + } + }; +#pragma pack(pop) + + /// + /// Highlight index + /// + class indexChr : public index + { + public: + /// + /// Constructs the index + /// + /// \param[in] h Reference to vector holding the data + /// + indexChr(_In_ std::vector &h) : index(h) {} + + /// + /// Compares two highlights by string (for searching) + /// + /// \param[in] a Pointer to first element + /// \param[in] b Pointer to second element + /// + /// \returns + /// - <0 when a < b + /// - =0 when a == b + /// - >0 when a > b + /// + virtual int compare(_In_ const highlight &a, _In_ const highlight &b) const + { + int r = ZRCola::CompareString(a.chr(), a.chr_len(), b.chr(), b.chr_len()); + if (r != 0) return r; + + if (a.set < b.set) return -1; + else if (a.set > b.set) return +1; + + return 0; + } + + /// + /// Compares two highlights by string (for sorting) + /// + /// \param[in] a Pointer to first element + /// \param[in] b Pointer to second element + /// + /// \returns + /// - <0 when a < b + /// - =0 when a == b + /// - >0 when a > b + /// + virtual int compare_sort(_In_ const highlight &a, _In_ const highlight &b) const + { + // Revert to `compare()` by default. + return compare(a, b); + } + } idxChr; ///< Highlight index + + + std::vector data; ///< Highlight data + + public: + /// + /// Constructs the database + /// + inline highlight_db() : idxChr(data) {} + + /// + /// Clears the database + /// + inline void clear() + { + idxChr.clear(); + data .clear(); + } + + /// + /// Highlights string + /// + /// \param[in] input Input string (UTF-16) + /// \param[in] inputMax Length of the input string in characters. Can be (size_t)-1 if \p input is zero terminated. + /// \param[in] callback Function to be called on highlight switch + /// + void Highlight(_In_z_count_(inputMax) const wchar_t* input, _In_ size_t inputMax, _In_ std::function callback) const; + }; + + + typedef stdex::idrec::record highlight_rec; +}; + + +const ZRCola::recordid_t ZRCola::highlight_rec::id = *(ZRCola::recordid_t*)"HGH"; + + +/// +/// Writes highlight database to a stream +/// +/// \param[in] stream Output stream +/// \param[in] db Highlight database +/// +/// \returns The stream \p stream +/// +inline std::ostream& operator <<(_In_ std::ostream& stream, _In_ const ZRCola::highlight_db &db) +{ + // Write highlight index. + if (stream.fail()) return stream; + stream << db.idxChr; + + // Write data count. + auto data_count = db.data.size(); +#if defined(_WIN64) || defined(__x86_64__) || defined(__ppc64__) + // 4G check + if (data_count > 0xffffffff) { + stream.setstate(std::ios_base::failbit); + return stream; + } +#endif + if (stream.fail()) return stream; + unsigned __int32 count = (unsigned __int32)data_count; + stream.write((const char*)&count, sizeof(count)); + + // Write data. + if (stream.fail()) return stream; + stream.write((const char*)db.data.data(), sizeof(unsigned __int16)*static_cast(count)); + + return stream; +} + + +/// +/// Reads highlight database from a stream +/// +/// \param[in ] stream Input stream +/// \param[out] db Highlight database +/// +/// \returns The stream \p stream +/// +inline std::istream& operator >>(_In_ std::istream& stream, _Out_ ZRCola::highlight_db &db) +{ + // Read highlight index. + stream >> db.idxChr; + if (!stream.good()) return stream; + + // Read data count. + unsigned __int32 count; + stream.read((char*)&count, sizeof(count)); + if (!stream.good()) return stream; + + if (count) { + // Read data. + db.data.resize(count); + stream.read((char*)db.data.data(), sizeof(unsigned __int16)*static_cast(count)); + } else + db.data.clear(); + + return stream; +} + +#pragma warning(pop) diff --git a/lib/libZRCola/src/highlight.cpp b/lib/libZRCola/src/highlight.cpp new file mode 100644 index 0000000..59717f2 --- /dev/null +++ b/lib/libZRCola/src/highlight.cpp @@ -0,0 +1,77 @@ +/* + SPDX-License-Identifier: GPL-3.0-or-later + Copyright © 2021 Amebis +*/ + +#include "pch.h" + +_Use_decl_annotations_ +void ZRCola::highlight_db::Highlight(const wchar_t* input, size_t inputMax, std::function callback) const +{ + size_t start = 0; + hlghtsetid_t set = ZRCOLA_HLGHTSETID_DEFAULT; + + for (size_t i = 0; i < inputMax;) { + // Find the longest matching highlight at i-th character. + size_t l_match = (size_t)-1; + for (size_t l = 0, r = idxChr.size(), ii = i, j = 0; ii < inputMax && l < r; ii++, j++) { + wchar_t c = input[ii]; + while (l < r) { + // Test the highlight in the middle of the search area. + size_t m = (l + r) / 2; + + // Get the j-th character of the highlight. + // All highlights that get short on characters are lexically ordered before. + // Thus the j-th character is considered 0. + wchar_t s = idxChr[m].chr_at(j); + + // Do the bisection test. + if (c < s) r = m; + else if (s < c) l = m + 1; + else { + // Character found. + + // Narrow the search area on the left to start at the first highlight in the run. + for (size_t r2 = m; l < r2;) { + size_t m2 = (l + r2) / 2; + if (c <= idxChr[m2].chr_at(j)) r2 = m2; else l = m2 + 1; + } + + // Narrow the search area on the right to end at the first highlight not in the run. + for (size_t l2 = m + 1; l2 < r;) { + size_t m2 = (l2 + r) / 2; + if (idxChr[m2].chr_at(j) <= c) l2 = m2 + 1; else r = m2; + } + + if (j + 1 == idxChr[l].chr_len()) { + // The first highlight of the run was a match (thus far). Save it. + l_match = l; + } + + break; + } + } + } + + if (l_match < idxChr.size()) { + // The saved highlight was an exact match. + const highlight &hghl = idxChr[l_match]; + if (set != hghl.set) { + callback(set, start, i); + start = i; + set = hghl.set; + } + i += hghl.chr_len(); + } else { + // The match was not found. + if (set != ZRCOLA_HLGHTSETID_DEFAULT) { + callback(set, start, i); + start = i; + set = ZRCOLA_HLGHTSETID_DEFAULT; + } + i++; + } + } + + callback(set, start, inputMax); +} diff --git a/lib/libZRCola/src/pch.h b/lib/libZRCola/src/pch.h index 56ddbd9..8787b18 100644 --- a/lib/libZRCola/src/pch.h +++ b/lib/libZRCola/src/pch.h @@ -8,6 +8,7 @@ #include "../../../include/version.h" #include "../include/zrcola/character.h" +#include "../include/zrcola/highlight.h" #include "../include/zrcola/language.h" #include "../include/zrcola/translate.h" #include "../include/zrcola/tag.h" diff --git a/output/data/ZRCola.zrcdb b/output/data/ZRCola.zrcdb index e5dd6ad0d24000eee74da957164d261d49f7e1df..d99d0cc39533062ac0875a33762ea7ed3b22e8da 100644 GIT binary patch delta 52823 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 J5}?2L{s-HVQ=tF= delta 17971 zcmY-04_sH(-3RdB{asOskdP432uO*Lkch|;5t5OSp^{@nNJMw!(4mnrLmx)u(9lT9 z=!ArXMn=Yr$CyJSM?{W{A#=`$HBx5Gh|m}@hmL%{=kr;0_v&?h|32rO?>XoG?&b2g zoi$$?Z*Tv?nE3F63mmg27f)=OXxz54==0eQhi|&!_(lv6Cj>bh!Q!>zTyeG-CEh09 zDc&P45L3ka#mB{D@fYGl;$|^Zd_{a*d{fL7o5iQZW8wy}S9~_zC~};a7h9y^Td`c6 za-+lXig>+PBhD4y5bqKj#Kq#f;v?c=akcn?_)D=vd_g=e?h;RlzZO3e-xvGEKZsw6 zpNS*sMTXy~Gd1AhAQ7Cmt6Q#ZzLs_?eh1_KQX0 zS7Nz1BJL3{Wf%ubbB(fz!8hM8chn<><4$q1_<-mkE*GbXKNtNTF%HH)Wwg2#O(~Z9 zN?NZML&dq`t>Rr`w76KDFFqnJ6jzIj#b1gk;tS$(u|~AScf^(A@5DT@TYOsln^+|N zL);{q&pI65&pHkkK4Y9S4MS!B%J4TVRYK}WJLxAbB^VM^Vg$;_g(b!;VU@;E+e(Ze zNlh9V4Lft=8dmKxPV4CTEUM3EGdzZrFkG?P!sIEVN> zhq_pj@f-?@NG%&%Ngo@{QVaK?q6Qf>~CPGL5D7Z^5~r zVB;u9dT-^4k(8~dTSqF%VbaSXK*7F{vf3q>mWeSx4eY7AYpR zq*Y_7zU|ncOF0NBM|mR2A*H0AbdUk!`XUNKNFvE0rKFy8kOAVlgLNd4O*dwwI}ceFMa`26Z7Mk>u2% zZYybEV<#CR?ysRPj3kj&xh;JS1NE#`q!>(&IhH7VHKXG{j1wkZ1W2x*nu%*qUUSp_ZZ{Soe5U;&R z^GG@=+KZ93dogD#=_AIQXbdFrB#RWkiOK5T#Phn7j1s?kG$xQ-QbAfsKXHEx=NI`F zCQD^1d<*4uZ0vXon;0Oj`;bCNBFQ18q<$aIeIL)Ac)iVYC+TlvNa5RQb2s8c|nCnn@2CBfkHO zA<-mFBOh_3k~Fighl~;51FSoM^)e3N%_=#7w{9OByAEJW!yM`PE;sltH^@}*E;p!= z4SCHum&sJzgt}VNO8SWL8}tSK#_*Fl<1~iK(2bTVWM4IDA-!Z=V=2Fb zAXX!Fq>xmT7LBEP56aFgb>*O(w55W6iw!3J7JWISRAZ=m(!s_7;@XT83vHGCnW2quidl(yR z)yUU99AdtQAwln05>ls;N7s9pdf+|m+M@+2vIXTSWF4t&!DP)$pOSGF1ip{D z1d{VUCfoKtW@&sMb=^!OdWfNXj-VihWFEoD;v*PYt1(pT5hGX~KVnQ%-ySih%DH*| z4&4zXndFgjjiDNuy2&u{Y{l%6B&8LzKfuO|$es^y ziX9q5U10KVlaC=w#gc4|p~~7Y$6Z+LI z`A0@HeT0F1GD6RM)CiaHabyjtIch{mWA{@r4>o<&qu0$NSs#d%UqH z$tdyeKw}h1A^D_&G?9~Jlz4y4I+8;2Nd;*lC&?)BK8C%_JBIf@^O&5UrOJ-s91b4C zu6s3xGCx5I(HLq8Q~oEYtNujZ`*%K(_x>-R;MN=Z1k-wSf{0FRE}7(!a@}XB1D%+* zrxVwCT=yBu?++lBWROBqOzLUkwtE;}{un9CgVg zkCc-}(oKemXEzEWNHWPI<)o2xlVRfdN7j*KlJ`e!Vh3qr<4H0~yicGmilmTyQbC%? zNis^j|HQgKVTDvuz{X0_OnS%|@jZ#UXp%+>PGXiS(!$1GGEV&dj3IF(>(7{_nAEbd zjr6n8Y>-Zl9tcoGc^jNY$q@wWV4#hB{4*zhY;BB>u1Hvq%XW>;8&4 zJ2aLW(v6mK?*(BbiR6+p($I^^x_VJQ!sPuK){FWKjcKHi)P9B!%A-12YVb2`*ZnjI zC&{PLmrp85%W2e|)yYtk&VbM}SUu?s*2`sM`58Qb51+yF%zrNt#Iy86&=5V7-_xuZN!qnDR z7_yn{BV8IpjWGEPVMq+g9Fni1o*0r(WjlwA=`!lT5bo4oX_vKJ&SQ}sjVjI1M7mfH7E)OBg(vJ#(*XpAA5WFy&2j*$x&F^l)Vu!-1zAzA;z zon7)TT-rL)&c<^b;y#8IBE~Qzc?<<>Nd-AbddOuK_+LWZ0p_VZfFx4=% zGYv9%I`w=i+KGZpjr>A}X&+NJ(?upf6C)RBES1Bwjj4(0Qzl~qMh0svl{5hhh>Mj@Ob!xtgbGmxc#i_q#+3bP|8Z?GF!8FF? zKM{Qi6ZJcxawj@N)H4&E@_{J-NW&3nSI2e3rHRgIvY_WAtQ$*mCSl5L8u>laBmEnfTO5CvCe$vAsS2!f-Y7{Ke$j=6t6jRI9DDP*Zn>&U?xT7&m zCqosnv5t+MY#d>u?-Y!TCplB(Yz$Vx!;yw$%)~O#D z$=5pdH)?sLoHUYdGE6+ZrNB~=-cJ2&Pu0m#1>PvHB+VL2^>E0zH})Mg4QH`r8q&IH zD6g7^cdb;=jyW5qV`rUY zh`9TrF^nXUTvA3FNGBO0?lV|711G&?23A;06gkYkUXC0melt-QOEO3ysU|I?myFNE z3jWt&g#}~<*?JxN8m{Asks;#lhrVz>)GZ_HNEQ29{7~0R#);qc+#tyyg{1m=%z5N` z%z2i{#UBMhB!Og;5>n@nf)0ON@L?wJ0Mtd16p~LWNE10pMgy?APar0XA(>=jAo}V8 zF=spJCoVT|gCv1ulM+%#+DSignZ-JiK(a{*sUz*Pu==@K*qPgG+pW~nAEq?e2nzYs1&GD1*S6vB6cshxfO#AOZ&g63f4 z!Z{eZY7XAd@;P`vo7vdQk>kWKlnapzQb?*vODN_%9f~>4n~{QUMqL8QCMBefv~%RS zn^ET$hPtpY)FqKzQbrm`CmAB{b5RgB7v)Px9vjO^Bk3l?#4}t9v(Nt)Y--; zMWSFHNsq)i6h`74s!0p!CF8{JHjIqB4doWuO!kp3GD3W!xHFPTHj=&MSQO?Qio)vd zxAWqVB$7+YNCWA-9d#FO#|mEact#|h6p=mTC>bOkKSDtyNhKRd4QV6ih+8xY!pSnS zj#QB&vj-@^ImTSc~!0~$-6V!F)aACHj>$O^JG9uKQV{T+^>dWdm1Qi#S<$xH=I zwM-p%W3r*Uu^0CR*h}~VG%h3SNEQ297VzAeuCQ;`Jt$a6R^7t~=RJII-XmXi$v1BI z;A<(51P~*rr3vyy;Q9o7nY)ASM>O*DLE+_lrFYJ~&Z$xydoLEvxEI?ix)-bMAxFue zMt&}~5Pgv(bs-807NWe8=`j0F6Z1Y41Z(80ZKgF$JD3jMhgo{=!z^P=GZRr4M=Y{g zBR@T0>S7vU^0^RmT2ThCQK>`%eEw8+5RM4M32S1dV*TISYN%xa`g)@5-hrJ2^Q@lBgAJZwh*H+R3_6#roBs1*Rd3{ z3=r33q>yA3Bqd|B`An7UYbHHpjQBo?y66W{m+>I#N*=^M_pz~yj1Zq?=!;#3k=dk- zG?8B7@?#7MBPpbi)R9gys*zuor=TD~BfrsOs$gni>SuEQ2}VX~GlVqF( zrJ^8-mjr+AsYm>{UPZ-#O@Ppzd}OOq`$J1(HZUsV41YhZ{nsGH z6Kf4dZq~_C^=nYj!8FJ|k0)e^e8I1gpCOSAq=vMSbHptVlZEBs&03a+`=KZgZ)-g{ z!Nw~(St{g7oI~=HNE@D%J7@coa_1a+Qtll2!wG4Sm9IRB`GVGBj|n83l#n{oPWsnk z$F5Id_K2r&8YxeqJfEqOeXTmlk9gSUl8+%lB!Og;l6;J;&&QnIOrz}c{sroyNXjp; zUcoOgvQ{VggC6$v6PI;dXq~)YhFZ7|BiF3M*;cK?`_)b^uyN+oc!v_6#>iYr>Y1nY zhuG8n5PMomEptx znaom$*UPmzzFwBRB>zZ*#|G3yl2o#R)Q~ofq0ViP%O}4<{vTY$NR6e^n2MO{n2s?G zGx-!^WUNMhe#TVFw4doD(`BZ>Ut;7!jeJBf?O9X+B$?!GM)?lX#Kx0M7m4q4sEd9M zBh#P5>P1Yo>}%D?uLg)wDkCj5s}v&_GUb$FNGa1k_H~uwq(@6}Vt!kk;p&zx@;j8r zod(%8x#e@l3n<_J0_HqHE|Qts z&=|K3L$bDEmSR%N#x^0wav1WT={SGG9gaz791douBV^X(Tdow@(+_=fX1VG<<-gsj z|8z8VyG$;B2}%k{HEAKeWSsbw%ccw!S1wzW2Le+`IcBLN?WCW$yokOal0dRa38{M# za~^#Wa}E-Z9Y~QRm24n2q>Y>-ZZDxA{3T45{1TSUBju!#bdzD?S%HxeB$?!qa?(h; z$uRMJnRO(Y@|@l>oM$s~`IlSa}_hKc7ZtRu-JkCc-}(oKemXC>=MGRY(5 zq>*%!VdA-ybtIYOk#f>Vx_8Q3ZmE%-_!#i50&!Jn%#x&@sFKfFJFD=^%0_mdV)s}T z7V&*muATgO=&LBTUPWmM8|&HF#l~UcRgHpq)fkypjk=<0JQnNO*I6x7%im5hdG5lH zdAnG!3w;H=anfrT8S)yYPGnm38fGac z2ie%ebmcW{H>4Irmeis=uNDhc)Z#(X!oIWYbJ>l)klpA@-i^9-yHQukz7{r~Wuwa; z7VO~)d$2Z z+p_71lF=~^{X8|^)Vtm6|EBYx|C?rRx$LeidV=0^BRLs8L4Q**9z8*Sc^GmBQt}-W zX376Tt^b?gn0R@u%CJ({YV##=Zehv&i%N zZl`0aIn|cDOfOry*1Xo1yiIRgnr2S3B_GqrmZqE2ZOPa4wWS&64Ey=dG-uj6uQRW+ zB|p>8maaFiwh#_D zKV&{+>r6A#Z0TY1VOvT!ciPf&bNPRz4qM7FGi>P*^ATIhG&7O@bN-K-kJ>u3%&h-P zzqh5w%*SlWGA&#Bsrge|$~LoY>2dRMTUueRU@F4f|1ZZn4Fqhr zr7r{Cv!%g+7F#+W@aFev^W*Uo-o_{uRuj+t^?Cx@RL*URBAM}QoGa@-)tEIDS&5hTZras