1 /* This file is (c) 2008-2012 Konstantin Isakov <ikm@goldendict.org>
2  * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */
3 
4 #include "article_maker.hh"
5 #include "config.hh"
6 #include "htmlescape.hh"
7 #include "utf8.hh"
8 #include "wstring_qt.hh"
9 #include <limits.h>
10 #include <QFile>
11 #include <QUrl>
12 #include <QTextDocumentFragment>
13 #include "folding.hh"
14 #include "langcoder.hh"
15 #include "gddebug.hh"
16 #include "qt4x5.hh"
17 
18 using std::vector;
19 using std::string;
20 using gd::wstring;
21 using std::set;
22 using std::list;
23 
ArticleMaker(vector<sptr<Dictionary::Class>> const & dictionaries_,vector<Instances::Group> const & groups_,QString const & displayStyle_,QString const & addonStyle_)24 ArticleMaker::ArticleMaker( vector< sptr< Dictionary::Class > > const & dictionaries_,
25                             vector< Instances::Group > const & groups_,
26                             QString const & displayStyle_,
27                             QString const & addonStyle_):
28   dictionaries( dictionaries_ ),
29   groups( groups_ ),
30   displayStyle( displayStyle_ ),
31   addonStyle( addonStyle_ ),
32   needExpandOptionalParts( true )
33 , collapseBigArticles( true )
34 , articleLimitSize( 500 )
35 {
36 }
37 
setDisplayStyle(QString const & st,QString const & adst)38 void ArticleMaker::setDisplayStyle( QString const & st, QString const & adst )
39 {
40   displayStyle = st;
41   addonStyle = adst;
42 }
43 
makeHtmlHeader(QString const & word,QString const & icon,bool expandOptionalParts) const44 std::string ArticleMaker::makeHtmlHeader( QString const & word,
45                                           QString const & icon,
46                                           bool expandOptionalParts ) const
47 {
48   string result =
49     "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
50     "<html><head>"
51     "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">";
52 
53   // Add a css stylesheet
54 
55   {
56     QFile builtInCssFile( ":/article-style.css" );
57     builtInCssFile.open( QFile::ReadOnly );
58     QByteArray css = builtInCssFile.readAll();
59 
60     if( !css.isEmpty() )
61     {
62       result += "\n<!-- Built-in css -->\n";
63       result += "<style type=\"text/css\" media=\"all\">\n";
64       result += css.data();
65       result += "</style>\n";
66     }
67 
68     if ( displayStyle.size() )
69     {
70       // Load an additional stylesheet
71       QFile builtInCssFile( QString( ":/article-style-st-%1.css" ).arg( displayStyle ) );
72       builtInCssFile.open( QFile::ReadOnly );
73       css = builtInCssFile.readAll();
74       if( !css.isEmpty() )
75       {
76         result += "<!-- Built-in style css -->\n";
77         result += "<style type=\"text/css\" media=\"all\">\n";
78         result += css.data();
79         result += "</style>\n";
80       }
81     }
82 
83     QFile cssFile( Config::getUserCssFileName() );
84 
85     if ( cssFile.open( QFile::ReadOnly ) )
86     {
87       css = cssFile.readAll();
88       if( !css.isEmpty() )
89       {
90         result += "<!-- User css -->\n";
91         result += "<style type=\"text/css\" media=\"all\">\n";
92         result += css.data();
93         result += "</style>\n";
94       }
95     }
96 
97     if( !addonStyle.isEmpty() )
98     {
99       QString name = Config::getStylesDir() + addonStyle
100                      + QDir::separator() + "article-style.css";
101       QFile addonCss( name );
102       if( addonCss.open( QFile::ReadOnly ) )
103       {
104         css = addonCss.readAll();
105         if( !css.isEmpty() )
106         {
107           result += "<!-- Addon style css -->\n";
108           result += "<style type=\"text/css\" media=\"all\">\n";
109           result += css.data();
110           result += "</style>\n";
111         }
112       }
113     }
114 
115     // Turn on/off expanding of article optional parts
116     if( expandOptionalParts )
117     {
118       result += "<!-- Expand optional parts css -->\n";
119       result += "<style type=\"text/css\" media=\"all\">\n";
120       result += "\n.dsl_opt\n{\n  display: inline;\n}\n\n.hidden_expand_opt\n{\n  display: none;\n}\n";
121       result += "</style>\n";
122     }
123 
124   }
125 
126   // Add print-only css
127 
128   {
129     QFile builtInCssFile( ":/article-style-print.css" );
130     builtInCssFile.open( QFile::ReadOnly );
131     QByteArray css = builtInCssFile.readAll();
132     if( !css.isEmpty() )
133     {
134       result += "<!-- Built-in print css -->\n";
135       result += "<style type=\"text/css\" media=\"print\">\n";
136       result += css.data();
137       result += "</style>\n";
138     }
139 
140     QFile cssFile( Config::getUserCssPrintFileName() );
141 
142     if ( cssFile.open( QFile::ReadOnly ) )
143     {
144       css = cssFile.readAll();
145       if( !css.isEmpty() )
146       {
147         result += "<!-- User print css -->\n";
148         result += "<style type=\"text/css\" media=\"print\">\n";
149         result += css.data();
150         result += "</style>\n";
151         css.clear();
152       }
153     }
154 
155     if( !addonStyle.isEmpty() )
156     {
157       QString name = Config::getStylesDir() + addonStyle
158                      + QDir::separator() + "article-style-print.css";
159       QFile addonCss( name );
160       if( addonCss.open( QFile::ReadOnly ) )
161       {
162         css = addonCss.readAll();
163         if( !css.isEmpty() )
164         {
165           result += "<!-- Addon style print css -->\n";
166           result += "<style type=\"text/css\" media=\"print\">\n";
167           result += css.data();
168           result += "</style>\n";
169         }
170       }
171     }
172   }
173 
174   result += "<title>" + Html::escape( Utf8::encode( gd::toWString( word ) ) ) + "</title>";
175 
176   // This doesn't seem to be much of influence right now, but we'll keep
177   // it anyway.
178   if ( icon.size() )
179     result += "<link rel=\"icon\" type=\"image/png\" href=\"qrcx://localhost/flags/" + Html::escape( icon.toUtf8().data() ) + "\" />\n";
180 
181   result += "<script type=\"text/javascript\">"
182             "var gdAudioLinks = { first: null, current: null };"
183             "function gdMakeArticleActive( newId ) {"
184             "if ( gdCurrentArticle != 'gdfrom-' + newId ) {"
185             "el=document.getElementById( gdCurrentArticle ); el.className = el.className.replace(' gdactivearticle','');"
186             "el=document.getElementById( 'gdfrom-' + newId ); el.className = el.className + ' gdactivearticle';"
187             "gdCurrentArticle = 'gdfrom-' + newId; gdAudioLinks.current = newId;"
188             "articleview.onJsActiveArticleChanged(gdCurrentArticle); } }"
189             "var overIframeId = null;"
190             "function gdSelectArticle( id ) {"
191             "var selection = window.getSelection(); var range = document.createRange();"
192             "range.selectNodeContents(document.getElementById('gdfrom-' + id));"
193             "selection.removeAllRanges(); selection.addRange(range); }"
194             "function processIframeMouseOut() { overIframeId = null; top.focus(); }"
195             "function processIframeMouseOver( newId ) { overIframeId = newId; }"
196             "function processIframeClick() { if( overIframeId != null ) { overIframeId = overIframeId.replace( 'gdexpandframe-', '' ); gdMakeArticleActive( overIframeId ) } }"
197             "function init() { window.addEventListener('blur', processIframeClick, false); }"
198             "window.addEventListener('load', init, false);"
199             "function gdExpandOptPart( expanderId, optionalId ) {  var d1=document.getElementById(expanderId); var i = 0; if( d1.alt == '[+]' ) {"
200             "d1.alt = '[-]'; d1.src = 'qrcx://localhost/icons/collapse_opt.png'; for( i = 0; i < 1000; i++ ) { var d2=document.getElementById( optionalId + i ); if( !d2 ) break; d2.style.display='inline'; } }"
201             "else { d1.alt = '[+]'; d1.src = 'qrcx://localhost/icons/expand_opt.png'; for( i = 0; i < 1000; i++ ) { var d2=document.getElementById( optionalId + i ); if( !d2 ) break; d2.style.display='none'; } } };"
202             "function gdExpandArticle( id ) { elem = document.getElementById('gdarticlefrom-'+id); ico = document.getElementById('expandicon-'+id); art=document.getElementById('gdfrom-'+id);"
203             "ev=window.event; t=null;"
204             "if(ev) t=ev.target || ev.srcElement;"
205             "if(elem.style.display=='inline' && t==ico) {"
206             "elem.style.display='none'; ico.className='gdexpandicon';"
207             "art.className = art.className+' gdcollapsedarticle';"
208             "nm=document.getElementById('gddictname-'+id); nm.style.cursor='pointer';"
209             "if(ev) ev.stopPropagation(); ico.title=''; nm.title=\"";
210   result += tr( "Expand article" ).toUtf8().data();
211   result += "\" } else if(elem.style.display=='none') {"
212             "elem.style.display='inline'; ico.className='gdcollapseicon';"
213             "art.className=art.className.replace(' gdcollapsedarticle','');"
214             "nm=document.getElementById('gddictname-'+id); nm.style.cursor='default';"
215             "nm.title=''; ico.title=\"";
216   result += tr( "Collapse article").toUtf8().data();
217   result += "\" } }"
218             "function gdCheckArticlesNumber() {"
219             "elems=document.getElementsByClassName('gddictname');"
220             "if(elems.length == 1) {"
221               "el=elems.item(0); s=el.id.replace('gddictname-','');"
222               "el=document.getElementById('gdfrom-'+s);"
223               "if(el && el.className.search('gdcollapsedarticle')>0) gdExpandArticle(s);"
224             "} }"
225             "</script>";
226 
227   result += "</head><body>";
228 
229   return result;
230 }
231 
makeNotFoundBody(QString const & word,QString const & group)232 std::string ArticleMaker::makeNotFoundBody( QString const & word,
233                                             QString const & group )
234 {
235   string result( "<div class=\"gdnotfound\"><p>" );
236 
237   QString str( word );
238   if( str.isRightToLeft() )
239   {
240     str.insert( 0, (ushort)0x202E ); // RLE, Right-to-Left Embedding
241     str.append( (ushort)0x202C ); // PDF, POP DIRECTIONAL FORMATTING
242   }
243 
244   if ( word.size() )
245     result += tr( "No translation for <b>%1</b> was found in group <b>%2</b>." ).
246               arg( QString::fromUtf8( Html::escape( str.toUtf8().data() ).c_str() ) ).
247               arg( QString::fromUtf8( Html::escape( group.toUtf8().data() ).c_str() ) ).
248                 toUtf8().data();
249   else
250     result += tr( "No translation was found in group <b>%1</b>." ).
251               arg( QString::fromUtf8( Html::escape( group.toUtf8().data() ).c_str() ) ).
252                 toUtf8().data();
253 
254   result += "</p></div>";
255 
256   return result;
257 }
258 
makeDefinitionFor(QString const & inWord,unsigned groupId,QMap<QString,QString> const & contexts,QSet<QString> const & mutedDicts,QStringList const & dictIDs,bool ignoreDiacritics) const259 sptr< Dictionary::DataRequest > ArticleMaker::makeDefinitionFor(QString const & inWord, unsigned groupId,
260   QMap< QString, QString > const & contexts,
261   QSet< QString > const & mutedDicts,
262   QStringList const & dictIDs , bool ignoreDiacritics ) const
263 {
264   if( !dictIDs.isEmpty() )
265   {
266     QStringList ids = dictIDs;
267     std::vector< sptr< Dictionary::Class > > ftsDicts;
268 
269     // Find dictionaries by ID's
270     for( unsigned x = 0; x < dictionaries.size(); x++ )
271     {
272       for( QStringList::Iterator it = ids.begin(); it != ids.end(); ++it )
273       {
274         if( *it == QString::fromStdString( dictionaries[ x ]->getId() ) )
275         {
276           ftsDicts.push_back( dictionaries[ x ] );
277           ids.erase( it );
278           break;
279         }
280       }
281       if( ids.isEmpty() )
282         break;
283     }
284 
285     string header = makeHtmlHeader( inWord.trimmed(), QString(), true );
286 
287     return new ArticleRequest( inWord.trimmed(), "",
288                                contexts, ftsDicts, header,
289                                -1, true );
290   }
291 
292   if ( groupId == Instances::Group::HelpGroupId )
293   {
294     // This is a special group containing internal welcome/help pages
295     string result = makeHtmlHeader( inWord, QString(), needExpandOptionalParts );
296 
297     if ( inWord == tr( "Welcome!" ) )
298     {
299       result += tr(
300 "<h3 align=\"center\">Welcome to <b>GoldenDict</b>!</h3>"
301 "<p>To start working with the program, first visit <b>Edit|Dictionaries</b> to add some directory paths where to search "
302 "for the dictionary files, set up various Wikipedia sites or other sources, adjust dictionary order or create dictionary groups."
303 "<p>And then you're ready to look up your words! You can do that in this window "
304 "by using a pane to the left, or you can <a href=\"Working with popup\">look up words from other active applications</a>. "
305 "<p>To customize program, check out the available preferences at <b>Edit|Preferences</b>. "
306 "All settings there have tooltips, be sure to read them if you are in doubt about anything."
307 "<p>Should you need further help, have any questions, "
308 "suggestions or just wonder what the others think, you are welcome at the program's <a href=\"http://goldendict.org/forum/\">forum</a>."
309 "<p>Check program's <a href=\"http://goldendict.org/\">website</a> for the updates. "
310 "<p>(c) 2008-2013 Konstantin Isakov. Licensed under GPLv3 or later."
311 
312         ).toUtf8().data();
313     }
314     else
315     if ( inWord == tr( "Working with popup" ) )
316     {
317       result += ( tr( "<h3 align=\"center\">Working with the popup</h3>"
318 
319 "To look up words from other active applications, you would need to first activate the <i>\"Scan popup functionality\"</i> in <b>Preferences</b>, "
320 "and then enable it at any time either by triggering the 'Popup' icon above, or "
321 "by clicking the tray icon down below with your right mouse button and choosing so in the menu you've popped. " ) +
322 
323 #ifdef Q_OS_WIN32
324   tr( "Then just stop the cursor over the word you want to look up in another application, "
325        "and a window would pop up which would describe it to you." )
326 #else
327   tr( "Then just select any word you want to look up in another application by your mouse "
328       "(double-click it or swipe it with mouse with the button pressed), "
329       "and a window would pop up which would describe the word to you." )
330 #endif
331   ).toUtf8().data();
332     }
333     else
334     {
335       // Not found
336       return makeNotFoundTextFor( inWord, "help" );
337     }
338 
339     result += "</body></html>";
340 
341     sptr< Dictionary::DataRequestInstant > r = new Dictionary::DataRequestInstant( true );
342 
343     r->getData().resize( result.size() );
344     memcpy( &( r->getData().front() ), result.data(), result.size() );
345 
346     return r;
347   }
348 
349   // Find the given group
350 
351   Instances::Group const * activeGroup = 0;
352 
353   for( unsigned x = 0; x < groups.size(); ++x )
354     if ( groups[ x ].id == groupId )
355     {
356       activeGroup = &groups[ x ];
357       break;
358     }
359 
360   // If we've found a group, use its dictionaries; otherwise, use the global
361   // heap.
362   std::vector< sptr< Dictionary::Class > > const & activeDicts =
363     activeGroup ? activeGroup->dictionaries : dictionaries;
364 
365   string header = makeHtmlHeader( inWord.trimmed(),
366                                   activeGroup && activeGroup->icon.size() ?
367                                     activeGroup->icon : QString(),
368                                   needExpandOptionalParts );
369 
370   if ( mutedDicts.size() )
371   {
372     std::vector< sptr< Dictionary::Class > > unmutedDicts;
373 
374     unmutedDicts.reserve( activeDicts.size() );
375 
376     for( unsigned x = 0; x < activeDicts.size(); ++x )
377       if ( !mutedDicts.contains(
378               QString::fromStdString( activeDicts[ x ]->getId() ) ) )
379         unmutedDicts.push_back( activeDicts[ x ] );
380 
381     return new ArticleRequest( inWord.trimmed(), activeGroup ? activeGroup->name : "",
382                                contexts, unmutedDicts, header,
383                                collapseBigArticles ? articleLimitSize : -1,
384                                needExpandOptionalParts, ignoreDiacritics );
385   }
386   else
387     return new ArticleRequest( inWord.trimmed(), activeGroup ? activeGroup->name : "",
388                                contexts, activeDicts, header,
389                                collapseBigArticles ? articleLimitSize : -1,
390                                needExpandOptionalParts, ignoreDiacritics );
391 }
392 
makeNotFoundTextFor(QString const & word,QString const & group) const393 sptr< Dictionary::DataRequest > ArticleMaker::makeNotFoundTextFor(
394   QString const & word, QString const & group ) const
395 {
396   string result = makeHtmlHeader( word, QString(), true ) + makeNotFoundBody( word, group ) +
397     "</body></html>";
398 
399   sptr< Dictionary::DataRequestInstant > r = new Dictionary::DataRequestInstant( true );
400 
401   r->getData().resize( result.size() );
402   memcpy( &( r->getData().front() ), result.data(), result.size() );
403 
404   return r;
405 }
406 
makeEmptyPage() const407 sptr< Dictionary::DataRequest > ArticleMaker::makeEmptyPage() const
408 {
409   string result = makeHtmlHeader( tr( "(untitled)" ), QString(), true ) +
410     "</body></html>";
411 
412   sptr< Dictionary::DataRequestInstant > r =
413       new Dictionary::DataRequestInstant( true );
414 
415   r->getData().resize( result.size() );
416   memcpy( &( r->getData().front() ), result.data(), result.size() );
417 
418   return r;
419 }
420 
makePicturePage(string const & url) const421 sptr< Dictionary::DataRequest > ArticleMaker::makePicturePage( string const & url ) const
422 {
423   string result = makeHtmlHeader( tr( "(picture)" ), QString(), true )
424                   + "<a href=\"javascript: if(history.length>2) history.go(-1)\">"
425                   + "<img src=\"" + url + "\" /></a>"
426                   + "</body></html>";
427 
428   sptr< Dictionary::DataRequestInstant > r =
429       new Dictionary::DataRequestInstant( true );
430 
431   r->getData().resize( result.size() );
432   memcpy( &( r->getData().front() ), result.data(), result.size() );
433 
434   return r;
435 }
436 
setExpandOptionalParts(bool expand)437 void ArticleMaker::setExpandOptionalParts( bool expand )
438 {
439   needExpandOptionalParts = expand;
440 }
441 
setCollapseParameters(bool autoCollapse,int articleSize)442 void ArticleMaker::setCollapseParameters( bool autoCollapse, int articleSize )
443 {
444   collapseBigArticles = autoCollapse;
445   articleLimitSize = articleSize;
446 }
447 
448 
adjustFilePath(QString & fileName)449 bool ArticleMaker::adjustFilePath( QString & fileName )
450 {
451   QFileInfo info( fileName );
452   if( !info.isFile() )
453   {
454     QString dir = Config::getConfigDir();
455     dir.chop( 1 );
456     info.setFile( dir + fileName);
457     if( info.isFile() )
458     {
459       fileName = info.canonicalFilePath();
460       return true;
461     }
462   }
463   return false;
464 }
465 
466 //////// ArticleRequest
467 
ArticleRequest(QString const & word_,QString const & group_,QMap<QString,QString> const & contexts_,vector<sptr<Dictionary::Class>> const & activeDicts_,string const & header,int sizeLimit,bool needExpandOptionalParts_,bool ignoreDiacritics_)468 ArticleRequest::ArticleRequest(
469   QString const & word_, QString const & group_,
470   QMap< QString, QString > const & contexts_,
471   vector< sptr< Dictionary::Class > > const & activeDicts_,
472   string const & header,
473   int sizeLimit, bool needExpandOptionalParts_, bool ignoreDiacritics_ ):
474     word( word_ ), group( group_ ), contexts( contexts_ ),
475     activeDicts( activeDicts_ ),
476     altsDone( false ), bodyDone( false ), foundAnyDefinitions( false ),
477     closePrevSpan( false )
478 ,   articleSizeLimit( sizeLimit )
479 ,   needExpandOptionalParts( needExpandOptionalParts_ )
480 ,   ignoreDiacritics( ignoreDiacritics_ )
481 {
482   // No need to lock dataMutex on construction
483 
484   hasAnyData = true;
485 
486   data.resize( header.size() );
487   memcpy( &data.front(), header.data(), header.size() );
488 
489   // Accumulate main forms
490 
491   for( unsigned x = 0; x < activeDicts.size(); ++x )
492   {
493     sptr< Dictionary::WordSearchRequest > s = activeDicts[ x ]->findHeadwordsForSynonym( gd::toWString( word ) );
494 
495     connect( s.get(), SIGNAL( finished() ),
496              this, SLOT( altSearchFinished() ), Qt::QueuedConnection );
497 
498     altSearches.push_back( s );
499   }
500 
501   altSearchFinished(); // Handle any ones which have already finished
502 }
503 
altSearchFinished()504 void ArticleRequest::altSearchFinished()
505 {
506   if ( altsDone )
507     return;
508 
509   // Check every request for finishing
510   for( list< sptr< Dictionary::WordSearchRequest > >::iterator i =
511          altSearches.begin(); i != altSearches.end(); )
512   {
513     if ( (*i)->isFinished() )
514     {
515       // This one's finished
516       for( size_t count = (*i)->matchesCount(), x = 0; x < count; ++x )
517         alts.insert( (**i)[ x ].word );
518 
519       altSearches.erase( i++ );
520     }
521     else
522       ++i;
523   }
524 
525   if ( altSearches.empty() )
526   {
527 #ifdef QT_DEBUG
528     qDebug( "alts finished\n" );
529 #endif
530 
531     // They all've finished! Now we can look up bodies
532 
533     altsDone = true; // So any pending signals in queued mode won't mess us up
534 
535     vector< wstring > altsVector( alts.begin(), alts.end() );
536 
537 #ifdef QT_DEBUG
538     for( unsigned x = 0; x < altsVector.size(); ++x )
539     {
540       qDebug() << "Alt:" << gd::toQString( altsVector[ x ] );
541     }
542 #endif
543 
544     wstring wordStd = gd::toWString( word );
545 
546     if( activeDicts.size() <= 1 )
547       articleSizeLimit = -1; // Don't collapse article if only one dictionary presented
548 
549     for( unsigned x = 0; x < activeDicts.size(); ++x )
550     {
551       try
552       {
553         sptr< Dictionary::DataRequest > r =
554           activeDicts[ x ]->getArticle( wordStd, altsVector,
555                                         gd::toWString( contexts.value( QString::fromStdString( activeDicts[ x ]->getId() ) ) ),
556                                         ignoreDiacritics );
557 
558         connect( r.get(), SIGNAL( finished() ),
559                  this, SLOT( bodyFinished() ), Qt::QueuedConnection );
560 
561         bodyRequests.push_back( r );
562       }
563       catch( std::exception & e )
564       {
565         gdWarning( "getArticle request error (%s) in \"%s\"\n",
566                    e.what(), activeDicts[ x ]->getName().c_str() );
567       }
568     }
569 
570     bodyFinished(); // Handle any ones which have already finished
571   }
572 }
573 
findEndOfCloseDiv(const QString & str,int pos)574 int ArticleRequest::findEndOfCloseDiv( const QString &str, int pos )
575 {
576   for( ; ; )
577   {
578     int n1 = str.indexOf( "</div>", pos );
579     if( n1 <= 0 )
580       return n1;
581 
582     int n2 = str.indexOf( "<div ", pos );
583     if( n2 <= 0 || n2 > n1 )
584       return n1 + 6;
585 
586     pos = findEndOfCloseDiv( str, n2 + 1 );
587     if( pos <= 0 )
588       return pos;
589   }
590 }
591 
bodyFinished()592 void ArticleRequest::bodyFinished()
593 {
594   if ( bodyDone )
595     return;
596 
597   GD_DPRINTF( "some body finished\n" );
598 
599   bool wasUpdated = false;
600 
601   while ( bodyRequests.size() )
602   {
603     // Since requests should go in order, check the first one first
604     if ( bodyRequests.front()->isFinished() )
605     {
606       // Good
607 
608       GD_DPRINTF( "one finished.\n" );
609 
610       Dictionary::DataRequest & req = *bodyRequests.front();
611 
612       QString errorString = req.getErrorString();
613 
614       if ( req.dataSize() >= 0 || errorString.size() )
615       {
616         sptr< Dictionary::Class > const & activeDict =
617             activeDicts[ activeDicts.size() - bodyRequests.size() ];
618 
619         string dictId = activeDict->getId();
620 
621         string head;
622 
623         string gdFrom = "gdfrom-" + Html::escape( dictId );
624 
625         if ( closePrevSpan )
626         {
627           head += "</div></div><div style=\"clear:both;\"></div><span class=\"gdarticleseparator\"></span>";
628         }
629         else
630         {
631           // This is the first article
632           head += "<script type=\"text/javascript\">"
633                   "var gdCurrentArticle=\"" + gdFrom  + "\"; "
634                   "articleview.onJsActiveArticleChanged(gdCurrentArticle)</script>";
635         }
636 
637         bool collapse = false;
638         if( articleSizeLimit >= 0 )
639         {
640           try
641           {
642             Mutex::Lock _( dataMutex );
643             QString text = QString::fromUtf8( req.getFullData().data(), req.getFullData().size() );
644 
645             if( !needExpandOptionalParts )
646             {
647               // Strip DSL optional parts
648               int pos = 0;
649               for( ; ; )
650               {
651                 pos = text.indexOf( "<div class=\"dsl_opt\"" );
652                 if( pos > 0 )
653                 {
654                   int endPos = findEndOfCloseDiv( text, pos + 1 );
655                   if( endPos > pos)
656                     text.remove( pos, endPos - pos );
657                   else
658                     break;
659                 }
660                 else
661                   break;
662               }
663             }
664 
665             int size = QTextDocumentFragment::fromHtml( text ).toPlainText().length();
666             if( size > articleSizeLimit )
667               collapse = true;
668           }
669           catch(...)
670           {
671           }
672         }
673 
674         string jsVal = Html::escapeForJavaScript( dictId );
675         head += "<script type=\"text/javascript\">var gdArticleContents; "
676           "if ( !gdArticleContents ) gdArticleContents = \"" + jsVal +" \"; "
677           "else gdArticleContents += \"" + jsVal + " \";</script>";
678 
679         head += string( "<div class=\"gdarticle" ) +
680                 ( closePrevSpan ? "" : " gdactivearticle" ) +
681                 ( collapse ? " gdcollapsedarticle" : "" ) +
682                 "\" id=\"" + gdFrom +
683                 "\" onClick=\"gdMakeArticleActive( '" + jsVal + "' );\" " +
684                 " onContextMenu=\"gdMakeArticleActive( '" + jsVal + "' );\""
685                 + ">";
686 
687         closePrevSpan = true;
688 
689         head += string( "<div class=\"gddictname\" onclick=\"gdExpandArticle(\'" ) + dictId + "\');"
690           + ( collapse ? "\" style=\"cursor:pointer;" : "" )
691           + "\" id=\"gddictname-" + Html::escape( dictId ) + "\""
692           + ( collapse ? string( " title=\"" ) + tr( "Expand article" ).toUtf8().data() + "\"" : "" )
693           + "><span class=\"gddicticon\"><img src=\"gico://" + Html::escape( dictId )
694           + "/dicticon.png\"></span><span class=\"gdfromprefix\">"  +
695           Html::escape( tr( "From " ).toUtf8().data() ) + "</span><span class=\"gddicttitle\">" +
696           Html::escape( activeDict->getName().c_str() ) + "</span>"
697           + "<span class=\"collapse_expand_area\"><img src=\"qrcx://localhost/icons/blank.png\" class=\""
698           + ( collapse ? "gdexpandicon" : "gdcollapseicon" )
699           + "\" id=\"expandicon-" + Html::escape( dictId ) + "\""
700           + ( collapse ? "" : string( " title=\"" ) + tr( "Collapse article" ).toUtf8().data() + "\"" )
701           + "></span>" + "</div>";
702 
703         head += "<div class=\"gddictnamebodyseparator\"></div>";
704 
705         head += "<div class=\"gdarticlebody gdlangfrom-";
706         head += LangCoder::intToCode2( activeDict->getLangFrom() ).toLatin1().data();
707         head += "\" lang=\"";
708         head += LangCoder::intToCode2( activeDict->getLangTo() ).toLatin1().data();
709         head += "\"";
710         head += " style=\"display:";
711         head += collapse ? "none" : "inline";
712         head += string( "\" id=\"gdarticlefrom-" ) + Html::escape( dictId ) + "\">";
713 
714         if ( errorString.size() )
715         {
716           head += "<div class=\"gderrordesc\">" +
717             Html::escape( tr( "Query error: %1" ).arg( errorString ).toUtf8().data() )
718           + "</div>";
719         }
720 
721         Mutex::Lock _( dataMutex );
722 
723         size_t offset = data.size();
724 
725         data.resize( data.size() + head.size() + ( req.dataSize() > 0 ? req.dataSize() : 0 ) );
726 
727         memcpy( &data.front() + offset, head.data(), head.size() );
728 
729         try
730         {
731           if ( req.dataSize() > 0 )
732             bodyRequests.front()->getDataSlice( 0, req.dataSize(),
733                                                 &data.front() + offset + head.size() );
734         }
735         catch( std::exception & e )
736         {
737           gdWarning( "getDataSlice error: %s\n", e.what() );
738         }
739 
740         wasUpdated = true;
741 
742         foundAnyDefinitions = true;
743       }
744       GD_DPRINTF( "erasing..\n" );
745       bodyRequests.pop_front();
746       GD_DPRINTF( "erase done..\n" );
747     }
748     else
749     {
750       GD_DPRINTF( "one not finished.\n" );
751       break;
752     }
753   }
754 
755   if ( bodyRequests.empty() )
756   {
757     // No requests left, end the article
758 
759     bodyDone = true;
760 
761     {
762       string footer;
763 
764       if ( closePrevSpan )
765       {
766         footer += "</div></div>";
767         closePrevSpan = false;
768       }
769 
770       if ( !foundAnyDefinitions )
771       {
772         // No definitions were ever found, say so to the user.
773 
774         // Larger words are usually whole sentences - don't clutter the output
775         // with their full bodies.
776         footer += ArticleMaker::makeNotFoundBody( word.size() < 40 ? word : "", group );
777 
778         // When there were no definitions, we run stemmed search.
779         stemmedWordFinder = new WordFinder( this );
780 
781         connect( stemmedWordFinder.get(), SIGNAL( finished() ),
782                  this, SLOT( stemmedSearchFinished() ), Qt::QueuedConnection );
783 
784         stemmedWordFinder->stemmedMatch( word, activeDicts );
785       }
786       else
787       {
788         footer += "</body></html>";
789       }
790 
791       Mutex::Lock _( dataMutex );
792 
793       size_t offset = data.size();
794 
795       data.resize( data.size() + footer.size() );
796 
797       memcpy( &data.front() + offset, footer.data(), footer.size() );
798     }
799 
800     if ( stemmedWordFinder.get() )
801       update();
802     else
803       finish();
804   }
805   else
806   if ( wasUpdated )
807     update();
808 }
809 
stemmedSearchFinished()810 void ArticleRequest::stemmedSearchFinished()
811 {
812   // Got stemmed matching results
813 
814   WordFinder::SearchResults sr = stemmedWordFinder->getResults();
815 
816   string footer;
817 
818   bool continueMatching = false;
819 
820   if ( sr.size() )
821   {
822     footer += "<div class=\"gdstemmedsuggestion\"><span class=\"gdstemmedsuggestion_head\">" +
823       Html::escape( tr( "Close words: " ).toUtf8().data() ) +
824       "</span><span class=\"gdstemmedsuggestion_body\">";
825 
826     for( unsigned x = 0; x < sr.size(); ++x )
827     {
828       footer += linkWord( sr[ x ].first );
829 
830       if ( x != sr.size() - 1 )
831       {
832         footer += ", ";
833       }
834     }
835 
836     footer += "</span></div>";
837   }
838 
839   splittedWords = splitIntoWords( word );
840 
841   if ( splittedWords.first.size() > 1 ) // Contains more than one word
842   {
843     disconnect( stemmedWordFinder.get(), SIGNAL( finished() ),
844                 this, SLOT( stemmedSearchFinished() ) );
845 
846     connect( stemmedWordFinder.get(), SIGNAL( finished() ),
847              this, SLOT( individualWordFinished() ), Qt::QueuedConnection );
848 
849     currentSplittedWordStart = -1;
850     currentSplittedWordEnd = currentSplittedWordStart;
851 
852     firstCompoundWasFound = false;
853 
854     compoundSearchNextStep( false );
855 
856     continueMatching = true;
857   }
858 
859   if ( !continueMatching )
860     footer += "</body></html>";
861 
862   {
863     Mutex::Lock _( dataMutex );
864 
865     size_t offset = data.size();
866 
867     data.resize( data.size() + footer.size() );
868 
869     memcpy( &data.front() + offset, footer.data(), footer.size() );
870   }
871 
872   if ( continueMatching )
873     update();
874   else
875     finish();
876 }
877 
compoundSearchNextStep(bool lastSearchSucceeded)878 void ArticleRequest::compoundSearchNextStep( bool lastSearchSucceeded )
879 {
880   if ( !lastSearchSucceeded )
881   {
882     // Last search was unsuccessful. First, emit what we had.
883 
884     string footer;
885 
886     if ( lastGoodCompoundResult.size() ) // We have something to append
887     {
888 //      DPRINTF( "Appending\n" );
889 
890       if ( !firstCompoundWasFound )
891       {
892         // Append the beginning
893         footer += "<div class=\"gdstemmedsuggestion\"><span class=\"gdstemmedsuggestion_head\">" +
894           Html::escape( tr( "Compound expressions: " ).toUtf8().data() ) +
895           "</span><span class=\"gdstemmedsuggestion_body\">";
896 
897         firstCompoundWasFound = true;
898       }
899       else
900       {
901         // Append the separator
902         footer += " / ";
903       }
904 
905       footer += linkWord( lastGoodCompoundResult );
906 
907       lastGoodCompoundResult.clear();
908     }
909 
910     // Then, start a new search for the next word, if possible
911 
912     if ( currentSplittedWordStart >= splittedWords.first.size() - 2 )
913     {
914       // The last word was the last possible to start from
915 
916       if ( firstCompoundWasFound )
917         footer += "</span>";
918 
919       // Now add links to all the individual words. They conclude the result.
920 
921       footer += "<div class=\"gdstemmedsuggestion\"><span class=\"gdstemmedsuggestion_head\">" +
922         Html::escape( tr( "Individual words: " ).toUtf8().data() ) +
923         "</span><span class=\"gdstemmedsuggestion_body\"";
924       if( splittedWords.first[ 0 ].isRightToLeft() )
925         footer += " dir=\"rtl\"";
926       footer += ">";
927 
928       footer += escapeSpacing( splittedWords.second[ 0 ] );
929 
930       for( int x = 0; x < splittedWords.first.size(); ++x )
931       {
932         footer += linkWord( splittedWords.first[ x ] );
933         footer += escapeSpacing( splittedWords.second[ x + 1 ] );
934       }
935 
936       footer += "</span>";
937 
938       footer += "</body></html>";
939 
940       appendToData( footer );
941 
942       finish();
943 
944       return;
945     }
946 
947     if ( footer.size() )
948     {
949       appendToData( footer );
950       update();
951     }
952 
953     // Advance to the next word and start from looking up two words
954     ++currentSplittedWordStart;
955     currentSplittedWordEnd = currentSplittedWordStart + 1;
956   }
957   else
958   {
959     // Last lookup succeeded -- see if we can try the larger sequence
960 
961     if ( currentSplittedWordEnd < splittedWords.first.size() - 1 )
962     {
963       // We can, indeed.
964       ++currentSplittedWordEnd;
965     }
966     else
967     {
968       // We can't. Emit what we have and start over.
969 
970       ++currentSplittedWordEnd; // So we could use the same code for result
971                                 // emitting
972 
973       // Initiate new lookup
974       compoundSearchNextStep( false );
975 
976       return;
977     }
978   }
979 
980   // Build the compound sequence
981 
982   currentSplittedWordCompound = makeSplittedWordCompound();
983 
984   // Look it up
985 
986 //  DPRINTF( "Looking up %s\n", qPrintable( currentSplittedWordCompound ) );
987 
988   stemmedWordFinder->expressionMatch( currentSplittedWordCompound, activeDicts, 40, // Would one be enough? Leave 40 to be safe.
989                                       Dictionary::SuitableForCompoundSearching );
990 }
991 
makeSplittedWordCompound()992 QString ArticleRequest::makeSplittedWordCompound()
993 {
994   QString result;
995 
996   result.clear();
997 
998   for( int x = currentSplittedWordStart; x <= currentSplittedWordEnd; ++x )
999   {
1000     result.append( splittedWords.first[ x ] );
1001 
1002     if ( x < currentSplittedWordEnd )
1003     {
1004       wstring ws( gd::toWString( splittedWords.second[ x + 1 ] ) );
1005 
1006       Folding::normalizeWhitespace( ws );
1007 
1008       result.append( gd::toQString( ws ) );
1009     }
1010   }
1011 
1012   return result;
1013 }
1014 
individualWordFinished()1015 void ArticleRequest::individualWordFinished()
1016 {
1017   WordFinder::SearchResults const & results = stemmedWordFinder->getResults();
1018 
1019   if ( results.size() )
1020   {
1021     wstring source = Folding::applySimpleCaseOnly( gd::toWString( currentSplittedWordCompound ) );
1022 
1023     bool hadSomething = false;
1024 
1025     for( unsigned x = 0; x < results.size(); ++x )
1026     {
1027       if ( results[ x ].second )
1028       {
1029         // Spelling suggestion match found. No need to continue.
1030         hadSomething = true;
1031         lastGoodCompoundResult = currentSplittedWordCompound;
1032         break;
1033       }
1034 
1035       // Prefix match found. Check if the aliases are acceptable.
1036 
1037       wstring result( Folding::applySimpleCaseOnly( gd::toWString( results[ x ].first ) ) );
1038 
1039       if ( source.size() <= result.size() && result.compare( 0, source.size(), source ) == 0 )
1040       {
1041         // The resulting string begins with the source one
1042 
1043         hadSomething = true;
1044 
1045         if ( source.size() == result.size() )
1046         {
1047           // Got the match. No need to continue.
1048           lastGoodCompoundResult = currentSplittedWordCompound;
1049           break;
1050         }
1051       }
1052     }
1053 
1054     if ( hadSomething )
1055     {
1056       compoundSearchNextStep( true );
1057       return;
1058     }
1059   }
1060 
1061   compoundSearchNextStep( false );
1062 }
1063 
appendToData(std::string const & str)1064 void ArticleRequest::appendToData( std::string const & str )
1065 {
1066   Mutex::Lock _( dataMutex );
1067 
1068   size_t offset = data.size();
1069 
1070   data.resize( data.size() + str.size() );
1071 
1072   memcpy( &data.front() + offset, str.data(), str.size() );
1073 
1074 }
1075 
splitIntoWords(QString const & input)1076 QPair< ArticleRequest::Words, ArticleRequest::Spacings > ArticleRequest::splitIntoWords( QString const & input )
1077 {
1078   QPair< Words, Spacings > result;
1079 
1080   QChar const * ptr = input.data();
1081 
1082   for( ; ; )
1083   {
1084     QString spacing;
1085 
1086     for( ; ptr->unicode() && ( Folding::isPunct( ptr->unicode() ) || Folding::isWhitespace( ptr->unicode() ) ); ++ptr )
1087       spacing.append( *ptr );
1088 
1089     result.second.append( spacing );
1090 
1091     QString word;
1092 
1093     for( ; ptr->unicode() && !( Folding::isPunct( ptr->unicode() ) || Folding::isWhitespace( ptr->unicode() ) ); ++ptr )
1094       word.append( *ptr );
1095 
1096     if ( word.isEmpty() )
1097       break;
1098 
1099     result.first.append( word );
1100   }
1101 
1102   return result;
1103 }
1104 
linkWord(QString const & str)1105 string ArticleRequest::linkWord( QString const & str )
1106 {
1107   QUrl url;
1108 
1109   url.setScheme( "gdlookup" );
1110   url.setHost( "localhost" );
1111   url.setPath( Qt4x5::Url::ensureLeadingSlash( str ) );
1112 
1113   string escapedResult = Html::escape( str.toUtf8().data() );
1114   return string( "<a href=\"" ) + url.toEncoded().data() + "\">" + escapedResult +"</a>";
1115 }
1116 
escapeSpacing(QString const & str)1117 std::string ArticleRequest::escapeSpacing( QString const & str )
1118 {
1119   QByteArray spacing = Html::escape( str.toUtf8().data() ).c_str();
1120 
1121   spacing.replace( "\n", "<br>" );
1122 
1123   return spacing.data();
1124 }
1125 
cancel()1126 void ArticleRequest::cancel()
1127 {
1128     if( isFinished() )
1129         return;
1130     if( !altSearches.empty() )
1131     {
1132         for( list< sptr< Dictionary::WordSearchRequest > >::iterator i =
1133                altSearches.begin(); i != altSearches.end(); ++i )
1134         {
1135             (*i)->cancel();
1136         }
1137     }
1138     if( !bodyRequests.empty() )
1139     {
1140         for( list< sptr< Dictionary::DataRequest > >::iterator i =
1141                bodyRequests.begin(); i != bodyRequests.end(); ++i )
1142         {
1143             (*i)->cancel();
1144         }
1145     }
1146     if( stemmedWordFinder.get() ) stemmedWordFinder->cancel();
1147     finish();
1148 }
1149