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