//+------------------------------------------------------------------------- // // THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF // ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A // PARTICULAR PURPOSE. // // Copyright 1998 - 2001 Microsoft Corporation. All Rights Reserved. // // PROGRAM: lrsample.cxx // // PURPOSE: Sample wordbreaker and stemmer. // // PLATFORM: Windows 2000 and later // //-------------------------------------------------------------------------- #include #include #include #include #include #include #include #include "lrsample.hxx" #include "filtreg.hxx" #include "langreg.hxx" //#define LEXICON_STEMMER //#define PORTER_STEMMER #define SIMPLE_LIST_STEMMER // The CLSID for the wordbreaker CLSID CLSID_SampleWordBreaker = /* d225281a-7ca9-4a46-ae7d-c63a9d4815d4 */ { 0xd225281a, 0x7ca9, 0x4a46, {0xae, 0x7d, 0xc6, 0x3a, 0x9d, 0x48, 0x15, 0xd4} }; // The CLSID of the stemmer CLSID CLSID_SampleStemmer = /* 0a275611-aa4d-4b39-8290-4baf77703f55 */ { 0x0a275611, 0xaa4d, 0x4b39, {0x82, 0x90, 0x4b, 0xaf, 0x77, 0x70, 0x3f, 0x55} }; // Global module refcount long g_cInstances = 0; HMODULE g_hModule = 0; #ifdef PORTER_STEMMER #include "porter.hxx" #endif //PORTER_STEMMER #ifdef LEXICON_STEMMER #include "stem.hxx" CStem * g_pStem = 0; #endif //LEXICON_STEMMER #ifdef SIMPLE_LIST_STEMMER // This is just a simple hard-coded list of words and stem forms. struct SStemForm { USHORT iList; // first index into aStems USHORT iForm; // second index into aStems }; const SStemForm aStemForms[] = { { 0, 0 }, // abide { 0, 2 }, // abided { 0, 4 }, // abides { 0, 3 }, // abiding { 0, 1 }, // abode { 1, 0 }, // bat { 2, 0 }, // batch { 2, 2 }, // batched { 2, 1 }, // batches { 2, 3 }, // batching { 1, 1 }, // bats { 1, 2 }, // batted { 1, 3 }, // batting { 3, 0 }, // bear { 3, 1 }, // bears { 4, 1 }, // began { 4, 0 }, // begin { 4, 3 }, // beginning { 4, 4 }, // begins { 4, 2 }, // begun { 3, 2 }, // bore { 3, 4 }, // born { 3, 3 }, // borne { 5, 0 }, // dance { 5, 1 }, // danced { 5, 2 }, // dances { 5, 3 }, // dancing { 6, 0 }, // heave { 6, 1 }, // heaved { 6, 3 }, // heaves { 6, 4 }, // heaving { 7, 0 }, // hero { 7, 1 }, // heroes { 6, 2 }, // hove { 8, 0 }, // keep { 8, 4 }, // keeping { 8, 1 }, // keeps { 8, 2 }, // kept { 9, 0 }, // misspell { 9, 1 }, // misspelled { 9, 3 }, // misspelling { 9, 4 }, // misspells { 9, 2 }, // misspelt { 10, 0 }, // plead { 10, 1 }, // pleaded { 10, 3 }, // pleading { 10, 4 }, // pleads { 10, 0 }, // pled { 11, 2 }, // ran { 11, 0 }, // run { 11, 3 }, // running { 11, 1 }, // runs { 12, 1 }, // swam { 12, 0 }, // swim { 12, 3 }, // swimming { 12, 4 }, // swims { 12, 2 }, // swum { 13, 2 }, // underlain { 13, 1 }, // underlay { 13, 0 }, // underlie { 13, 4 }, // underlies { 13, 3 }, // underlying }; const ULONG cStemForms = ArraySize( aStemForms ); const ULONG cMaxStemForms = 8; const WCHAR * aStems[][ cMaxStemForms ] = { { L"abide", L"abode", L"abided", L"abiding", L"abides" }, // 0 { L"bat", L"bats", L"batted", L"batting" }, // 1 { L"batch", L"batches", L"batched", L"batching" }, // 2 { L"bear", L"bears", L"bore", L"borne", L"born" }, // 3 { L"begin", L"began", L"begun", L"beginning", L"begins" }, // 4 { L"dance", L"danced", L"dances", L"dancing" }, // 5 { L"heave", L"heaved", L"hove", L"heaves", L"heaving" }, // 6 { L"hero", L"heroes" }, // 7 { L"keep", L"keeps", L"kept", L"keeping" }, // 8 { L"misspell", L"misspelled", L"misspelt", L"misspelling", L"misspells" }, // 9 { L"plead", L"pleaded", L"pled", L"pleading", L"pleads" }, // 10 { L"run", L"runs", L"ran", L"running" }, // 11 { L"swim", L"swam", L"swum", L"swimming", L"swims" }, // 12 { L"underlie", L"underlay", L"underlain", L"underlying", L"underlies" }, // 13 }; int __cdecl StemCompare( const void *p1, const void *p2 ) { SStemForm const * pForm = (SStemForm const *) p2; WCHAR const * pwcWord = (WCHAR const *) p1; return wcscmp( pwcWord, aStems[ pForm->iList ][ pForm->iForm ] ); } #endif // SIMPLE_LIST_STEMMER //+------------------------------------------------------------------------- // // Function: IsWordChar // // Synopsis: Find whether the i'th character in the buffer _pwcChunk // is a word character (rather than word break) // // Arguments: [pwcChunk] -- Characters whose type information is checked // [i] -- Index of character to check // [pInfo1] -- Type 1 information // [pInfo3] -- Type 3 information // // Returns: TRUE if the character is a word character // FALSE if it's a word breaking character // //-------------------------------------------------------------------------- __forceinline BOOL IsWordChar( WCHAR const * pwcChunk, int i, WORD const * pInfo1, WORD const * pInfo3 ) { // Any alphabetic, digit, or non-spacing character is part of a word if ( ( 0 != ( pInfo1[i] & ( C1_ALPHA | C1_DIGIT ) ) ) || ( 0 != ( pInfo3[i] & C3_NONSPACING ) ) ) return TRUE; WCHAR c = pwcChunk[i]; // Underscore is part of a word if ( L'_' == c ) return TRUE; // // A non-breaking space followed by a non-spacing character should not // be a word breaker. // if ( 0xa0 == c ) // non breaking space { // followed by a non-spacing character (looking ahead is okay) if ( 0 != ( pInfo3[i+1] & C3_NONSPACING ) ) return TRUE; } return FALSE; } //IsWordChar //+--------------------------------------------------------------------------- // // Function: ScanChunk // // Synopsis: For each character find its type information flags // // Arguments: [pwcChunk] -- Characters whose type information is retrieved // [cwc] -- Number of characters to scan // [pInfo1] -- Type 1 information is written here // [pInfo3] -- Type 3 information is written here // // Returns: S_OK if successful or an error code // //---------------------------------------------------------------------------- HRESULT ScanChunk( WCHAR const * pwcChunk, ULONG cwc, WORD * pInfo1, WORD * pInfo3 ) { if ( !GetStringTypeW( CT_CTYPE1, // POSIX character typing pwcChunk, // Source cwc, // Size of source pInfo1 ) ) // Character info 1 return HRESULT_FROM_WIN32( GetLastError() ); if ( !GetStringTypeW( CT_CTYPE3, // Additional POSIX pwcChunk, // Source cwc, // Size of source pInfo3 ) ) // Character info 3 return HRESULT_FROM_WIN32( GetLastError() ); return S_OK; } //ScanChunk //+--------------------------------------------------------------------------- // // Member: CSampleWordBreaker::Tokenize // // Synopsis: Break a block of text into individual words // // Arguments: [pTextSource] -- Source of characters to work on // [cwc] -- Number of characters to process // [pWordSink] -- Where to send the words found // [cwcProcessed] -- Returns the # of characters tokenized // // Returns: S_OK if successful or an error code // //---------------------------------------------------------------------------- HRESULT CSampleWordBreaker::Tokenize( TEXT_SOURCE * pTextSource, ULONG cwc, IWordSink * pWordSink, ULONG & cwcProcessed ) { // Leave space for one (unused) lookahead WORD aInfo1[ CSampleWordBreaker::cwcAtATime + 1 ]; WORD aInfo3[ CSampleWordBreaker::cwcAtATime + 1 ]; // Initialize this so we can go 1 beyond in IsWordChar() aInfo3 [ CSampleWordBreaker::cwcAtATime ] = C3_NONSPACING; // Get a pointer to the text we'll be working on const WCHAR * pwcChunk = &pTextSource->awcBuffer[ pTextSource->iCur ]; HRESULT hr = ScanChunk( pwcChunk, cwc, aInfo1, aInfo3 ); if ( FAILED( hr ) ) return hr; BOOL fWordHasZWS = FALSE; // Does the current word have a 0-width-space? ULONG cwcZWS; // Length of word minus embedded 0-width-spaces // // iBeginWord is the offset into aInfoX of the beginning character of // a word. iCur is the first unprocessed character. // They are indexes into the current block (_pwcChunk). // ULONG iBeginWord = 0; ULONG iCur = 0; // Temp buffer for a word having zero-width space WCHAR awcBufZWS[ CSampleWordBreaker::cwcAtATime ]; // Send words from the current block to word sink while ( iCur < cwc ) { // Skip whitespace, punctuation, etc. for (; iCur < cwc; iCur++) if ( IsWordChar( pwcChunk, iCur, aInfo1, aInfo3 ) ) break; // iCur points to a word char or is equal to cwc iBeginWord = iCur; if ( iCur < cwc ) iCur++; // we knew it pointed at word character // // Find word break. Filter may output Unicode zero-width-space, which // should be ignored by the wordbreaker. // fWordHasZWS = FALSE; for ( ; iCur < cwc; iCur++ ) { if ( !IsWordChar( pwcChunk, iCur, aInfo1, aInfo3 ) ) { if ( ZERO_WIDTH_SPACE == pwcChunk[iCur] ) fWordHasZWS = TRUE; else break; } } if ( fWordHasZWS ) { // Copy word into awcBufZWS after stripping zero-width-spaces cwcZWS = 0; for ( ULONG i = iBeginWord; i < iCur; i++ ) { if ( ZERO_WIDTH_SPACE != pwcChunk[i] ) awcBufZWS[cwcZWS++] = pwcChunk[i]; } } // iCur points to a non-word char or is equal to cwc if ( iCur < cwc ) { // store the word and its source position if ( fWordHasZWS ) hr = pWordSink->PutWord( cwcZWS, awcBufZWS, // stripped word iCur - iBeginWord, pTextSource->iCur + iBeginWord ); else hr = pWordSink->PutWord( iCur - iBeginWord, pwcChunk + iBeginWord, // the word iCur - iBeginWord, pTextSource->iCur + iBeginWord ); if ( FAILED( hr ) ) return hr; iCur++; // we knew it pointed at non-word char iBeginWord = iCur; // in case we exit the loop now } } // next word // End of words in chunk. // iCur == cwc // iBeginWord points at beginning of word or == cwc if ( 0 == iBeginWord ) { // A single word fills from beginning of this chunk // to the end. This is either a very long word or // a short word in a leftover buffer. // store the word and its source position if ( fWordHasZWS ) hr = pWordSink->PutWord( cwcZWS, awcBufZWS, // stripped word iCur, pTextSource->iCur ); // its source pos. else hr = pWordSink->PutWord( iCur, pwcChunk, // the word iCur, pTextSource->iCur ); // its source pos. if ( FAILED( hr ) ) return hr; // Position it to not add the word twice. iBeginWord = iCur; } // // If this is the last chunk from text source, then process the // last fragment. // if ( ( cwc < CSampleWordBreaker::cwcAtATime ) && ( iBeginWord != iCur ) ) { // store the word and its source position if ( fWordHasZWS ) hr = pWordSink->PutWord( cwcZWS, awcBufZWS, // stripped word iCur - iBeginWord, pTextSource->iCur + iBeginWord ); else hr = pWordSink->PutWord( iCur - iBeginWord, pwcChunk + iBeginWord, // the word iCur - iBeginWord, pTextSource->iCur + iBeginWord ); if ( FAILED( hr ) ) return hr; iBeginWord = iCur; } cwcProcessed = iBeginWord; return S_OK; } //Tokenize //+--------------------------------------------------------------------------- // // Member: CSampleWordBreaker::BreakText // // Synopsis: Break a block of text into individual words // // Arguments: [pTextSource] -- Source of characters to work on // [pWordSink] -- Where to send the words found // [pPhraseSink] -- Where to send the phrases found (not used) // // Returns: S_OK if successful or an error code // //---------------------------------------------------------------------------- HRESULT STDMETHODCALLTYPE CSampleWordBreaker::BreakText( TEXT_SOURCE * pTextSource, IWordSink * pWordSink, IPhraseSink * pPhraseSink ) { // Validate arguments if ( 0 == pTextSource ) return E_INVALIDARG; if ( ( 0 == pWordSink ) || ( pTextSource->iCur == pTextSource->iEnd ) ) return S_OK; if ( pTextSource->iCur > pTextSource->iEnd ) return E_INVALIDARG; ULONG cwcProcessed; // # chars actually processed by Tokenize() HRESULT hr = S_OK; // Pull text from the text source and tokenize it do { BOOL fFirstTime = TRUE; while ( pTextSource->iCur < pTextSource->iEnd ) { ULONG cwc = pTextSource->iEnd - pTextSource->iCur; // Process in buckets of cwcAtATime only if ( cwc >= CSampleWordBreaker::cwcAtATime ) cwc = CSampleWordBreaker::cwcAtATime; else if ( !fFirstTime ) break; hr = Tokenize( pTextSource, cwc, pWordSink, cwcProcessed ); if ( FAILED( hr ) ) return hr; pTextSource->iCur += cwcProcessed; fFirstTime = FALSE; } hr = pTextSource->pfnFillTextBuffer( pTextSource ); } while ( SUCCEEDED( hr ) ); // // If anything failed except for running out of text, report the error. // Otherwise, for cases like out of memory, files will not get retried or // reported as failures properly. // if ( ( FAILED( hr ) ) && ( FILTER_E_NO_MORE_VALUES != hr ) && ( FILTER_E_NO_TEXT != hr ) && ( FILTER_E_NO_VALUES != hr ) && ( FILTER_E_NO_MORE_TEXT != hr ) && ( FILTER_E_END_OF_CHUNKS != hr ) && ( WBREAK_E_END_OF_TEXT != hr ) ) return hr; ULONG cwc = pTextSource->iEnd - pTextSource->iCur; if ( 0 == cwc ) return S_OK; return Tokenize( pTextSource, cwc, pWordSink, cwcProcessed ); } //BreakText //+--------------------------------------------------------------------------- // // Member: CSampleStemmer::GenerateWordForms // // Synopsis: From the input word, emit the original and alternate forms // of the word. // // Arguments: [pwcInBuf] -- The original word to stem (not 0-terminated) // [cwc] -- Length in characters of the word // [pStemSink] -- Where to emit the stems // // Returns: S_OK if successful or an error code // //---------------------------------------------------------------------------- HRESULT STDMETHODCALLTYPE CSampleStemmer::GenerateWordForms( WCHAR const * pwcInBuf, ULONG cwc, IWordFormSink * pWordFormSink ) { // Validate the arguments if ( ( 0 == pwcInBuf ) || ( 0 == pWordFormSink ) ) return E_INVALIDARG; HRESULT hr = S_OK; #ifdef PORTER_STEMMER // // If the word is small enough, attempt to get the stemmed form of the // word. Emit both forms if they are different. The Porter algorithm // does the opposite of what's required here, but doing the right thing // requires a lexicon. // if ( cwc < cwcMaxPorterWord ) { // Make a temporary buffer for the word WCHAR awcPorter[ cwcMaxPorterWord ]; CopyMemory( awcPorter, pwcInBuf, sizeof(WCHAR) * cwc ); awcPorter[cwc] = 0; // Convert it to lowercase and save the original in lowercase CharLower( awcPorter ); WCHAR awcOriginal[ cwcMaxPorterWord ]; wcscpy( awcOriginal, awcPorter ); // Get the stemmed form of the word GetPorterStemForm( awcPorter ); // If it's different from the original, emit it. if ( wcscmp( awcOriginal, awcPorter ) ) { hr = pWordFormSink->PutAltWord( awcPorter, wcslen( awcPorter ) ); if ( FAILED( hr ) ) return hr; } } #endif //PORTER_STEMMER #ifdef LEXICON_STEMMER // // If the word is small enough to work with the stemmer, attempt to get // various forms of the word. // if ( cwc < cbMaxStem ) { // // Convert the original string to 8-bit characters. This is OK since // it's is an English stemmer that can safely assume such characters. // char acOriginal[ cbMaxStem ]; for ( unsigned i = 0; i < cwc; i++ ) acOriginal[ i ] = (char) pwcInBuf[ i ]; acOriginal[ i ] = 0; // Enumerate all stem-sets that contain the word. unsigned iBmk = stemInvalid; unsigned iStemSet = stemInvalid; char ac[ cbMaxStem ]; while ( g_pStem->FindStemSet( acOriginal, iBmk, iStemSet ) ) { // Enumerate all forms of the stem-set, root first. CStemSet set( g_pStem->GetStemSetRoot(), iStemSet ); unsigned iStemBmk = stemInvalid; while ( set.GetForm( ac, iStemBmk ) ) { if ( strcmp( ac, acOriginal ) ) { WCHAR awcForm[ cbMaxStem ]; mbstowcs( awcForm, ac, -1 ); hr = pWordFormSink->PutAltWord( awcForm, wcslen( awcForm ) ); if ( FAILED( hr ) ) return hr; } } } } #endif //LEXICON_STEMMER #ifdef SIMPLE_LIST_STEMMER // Look up the word in the simple list of stem forms SStemForm const * pStemForm = (SStemForm *) bsearch( pwcInBuf, aStemForms, cStemForms, sizeof SStemForm, StemCompare ); if ( 0 != pStemForm ) { // Found it, now iterate all the forms ULONG iList = pStemForm->iList; ULONG iForm = 0; while ( 0 != aStems[ iList ][ iForm ] ) { WCHAR const * pwc = aStems[ iList ][ iForm ]; // Don't emit the original word yet if ( 0 != wcscmp( pwc, pwcInBuf ) ) { hr = pWordFormSink->PutAltWord( pwc, wcslen( pwc ) ); if ( FAILED( hr ) ) return hr; } iForm++; } } #endif //SIMPLE_LIST_STEMMER // Emit the original word return pWordFormSink->PutWord( pwcInBuf, cwc ); } //StemWord //+------------------------------------------------------------------------- // // Method: CLanguageResourceSampleCF::CLanguageResourceSampleCF // // Synopsis: Language resource class factory constructor // //-------------------------------------------------------------------------- CLanguageResourceSampleCF::CLanguageResourceSampleCF() : _lRefs( 1 ) { InterlockedIncrement( &g_cInstances ); } //CLanguageResourceSampleCF //+------------------------------------------------------------------------- // // Method: CLanguageResourceSampleCF::~CLanguageResourceSampleCF // // Synopsis: Language resource class factory destructor // //-------------------------------------------------------------------------- CLanguageResourceSampleCF::~CLanguageResourceSampleCF() { InterlockedDecrement( &g_cInstances ); } //~LanguageResourceSampleCF //+------------------------------------------------------------------------- // // Method: CLanguageResourceSampleCF::QueryInterface // // Synopsis: Rebind to the requested interface // // Arguments: [riid] -- IID of new interface // [ppvObject] -- New interface * returned here // // Returns: S_OK if bind succeeded, E_NOINTERFACE if bind failed // //-------------------------------------------------------------------------- HRESULT STDMETHODCALLTYPE CLanguageResourceSampleCF::QueryInterface( REFIID riid, void ** ppvObject ) { if ( IID_IClassFactory == riid ) *ppvObject = (IUnknown *) (IClassFactory *) this; else if ( IID_IUnknown == riid ) *ppvObject = (IUnknown *) (IPersist *) this; else { *ppvObject = 0; return E_NOINTERFACE; } AddRef(); return S_OK; } //QueryInterface //+------------------------------------------------------------------------- // // Method: CLanguageResourceSampleCF::AddRef // // Synopsis: Increments the refcount // // Returns: The new refcount // //-------------------------------------------------------------------------- ULONG STDMETHODCALLTYPE CLanguageResourceSampleCF::AddRef() { return InterlockedIncrement( &_lRefs ); } //AddRef //+------------------------------------------------------------------------- // // Method: CLanguageResourceSampleCF::Release // // Synopsis: Decrement refcount. Delete self if necessary. // // Returns: The new refcount // //-------------------------------------------------------------------------- ULONG STDMETHODCALLTYPE CLanguageResourceSampleCF::Release() { long lTmp = InterlockedDecrement( &_lRefs ); if ( 0 == lTmp ) delete this; return lTmp; } //Release //+------------------------------------------------------------------------- // // Method: CLanguageResourceSampleCF::CreateInstance // // Synopsis: Creates new Language Resource sample object // // Arguments: [pUnkOuter] -- 'Outer' IUnknown // [riid] -- Interface to bind // [ppvObject] -- Interface returned here // // Returns: S_OK if successful or an appropriate error code // //-------------------------------------------------------------------------- HRESULT STDMETHODCALLTYPE CLanguageResourceSampleCF::CreateInstance( IUnknown * pUnkOuter, REFIID riid, void * * ppvObject ) { *ppvObject = 0; if ( IID_IStemmer == riid ) *ppvObject = new CSampleStemmer(); else if ( IID_IWordBreaker == riid ) *ppvObject = new CSampleWordBreaker(); else return E_NOINTERFACE; if ( 0 == *ppvObject ) return E_OUTOFMEMORY; return S_OK; } //CreateInstance //+------------------------------------------------------------------------- // // Method: CLanguageResourceSampleCF::LockServer // // Synopsis: Force class factory to remain loaded // // Arguments: [fLock] -- TRUE if locking, FALSE if unlocking // // Returns: S_OK // //-------------------------------------------------------------------------- HRESULT STDMETHODCALLTYPE CLanguageResourceSampleCF::LockServer( BOOL fLock ) { if ( fLock ) InterlockedIncrement( &g_cInstances ); else InterlockedDecrement( &g_cInstances ); return S_OK; } //LockServer //+------------------------------------------------------------------------- // // Function: DllGetClassObject // // Synopsis: Ole DLL load class routine // // Arguments: [cid] -- Class to load // [iid] -- Interface to bind to on class object // [ppvObj] -- Interface pointer returned here // // Returns: Sample language resource class factory // //-------------------------------------------------------------------------- extern "C" HRESULT STDMETHODCALLTYPE DllGetClassObject( REFCLSID cid, REFIID iid, void ** ppvObj ) { IUnknown * pUnk = 0; *ppvObj = 0; if ( CLSID_SampleWordBreaker == cid || CLSID_SampleStemmer == cid ) { pUnk = new CLanguageResourceSampleCF(); if ( 0 == pUnk ) return E_OUTOFMEMORY; #ifdef LEXICON_STEMMER if ( 0 == g_pStem ) g_pStem = MakeStemObject( g_hModule ); if ( 0 == g_pStem ) { pUnk->Release(); return E_OUTOFMEMORY; } #endif //LEXICON_STEMMER } else { *ppvObj = 0; return E_NOINTERFACE; } HRESULT hr = pUnk->QueryInterface( iid, ppvObj ); pUnk->Release(); return hr; } //DllGetClassObject //+------------------------------------------------------------------------- // // Function: DllCanUnloadNow // // Synopsis: Notifies DLL to unload (cleanup global resources) // // Returns: S_OK if it is acceptable for caller to unload DLL. // S_FALSE otherwise. // //-------------------------------------------------------------------------- extern "C" HRESULT STDMETHODCALLTYPE DllCanUnloadNow( void ) { if ( 0 == g_cInstances ) return S_OK; return S_FALSE; } //DllCanUnloadNow //+------------------------------------------------------------------------- // // Function: DllMain // // Synopsis: Standard main entry point for the module. // //-------------------------------------------------------------------------- BOOL WINAPI DllMain( HANDLE hInstance, DWORD dwReason, void * lpReserved ) { if ( DLL_PROCESS_ATTACH == dwReason ) { g_hModule = (HMODULE) hInstance; DisableThreadLibraryCalls( (HINSTANCE) hInstance ); } return TRUE; } //DllMain SLangRegistry const English_Sample_LangRes = { L"English_Sample", MAKELANGID( LANG_ENGLISH, SUBLANG_ENGLISH_SAMPLE ), { L"{d225281a-7ca9-4a46-ae7d-c63a9d4815d4}", L"English_Sample Word Breaker", L"lrsample.dll", L"both" }, { L"{0a275611-aa4d-4b39-8290-4baf77703f55}", L"English_Sample Stemmer", L"lrsample.dll", L"both" } }; //+------------------------------------------------------------------------- // // Method: DllRegisterServer // // Synopsis: Registers the language resources in the registry // //-------------------------------------------------------------------------- STDAPI DllRegisterServer() { return RegisterALanguageResource( English_Sample_LangRes ); } //DllRegisterServer //+------------------------------------------------------------------------- // // Method: DllUnregisterServer // // Synopsis: Removes the language resources from the registry // //-------------------------------------------------------------------------- STDAPI DllUnregisterServer() { return UnRegisterALanguageResource( English_Sample_LangRes ); } //DllUnregisterServer