forked from goldendict/goldendict
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy patharticle_maker.cc
1469 lines (1186 loc) · 49.1 KB
/
article_maker.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* This file is (c) 2008-2012 Konstantin Isakov <ikm@goldendict.org>
* Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */
#include "article_maker.hh"
#include "config.hh"
#include "htmlescape.hh"
#include "utf8.hh"
#include "wstring_qt.hh"
#include <limits.h>
#include <QFileInfo>
#include <QUrl>
#include <QTextDocumentFragment>
#include "folding.hh"
#include "langcoder.hh"
#include "gddebug.hh"
#include "qt4x5.hh"
#include <algorithm>
#ifndef USE_QTWEBKIT
#include <QColor>
#include <QFile>
#include <QMessageBox>
#include <QVarLengthArray>
#include <cctype>
#include <regex>
#endif
using std::vector;
using std::string;
using gd::wstring;
using std::set;
using std::list;
namespace {
void appendScripts( string & result )
{
result +=
// *blocking.js scripts block HTML parser, which is acceptable here,
// because the scripts are local, instantly available and fast.
#ifdef USE_QTWEBKIT
// Evaluate webkit_blocking.js now to call gdArticleView.onJsPageInitStarted() ASAP.
"<script src='qrc:///scripts/webkit_blocking.js'></script>"
#else
// Create QWebChannel now to make gdArticleView available ASAP.
"<script src='qrc:///qtwebchannel/qwebchannel.js'></script>"
"<script src='qrc:///scripts/webengine_blocking.js'></script>"
#endif
// Start reading the deferred scripts early so that they are ready when needed.
"<script defer src='qrc:///scripts/deferred.js'></script>"
#ifndef USE_QTWEBKIT
// Load webengine_deferred.js in the end because it calls gdArticleView.onJsPageInitFinished().
"<script defer src='qrc:///scripts/webengine_deferred.js'></script>"
#endif
"<script>"
"const gdExpandArticleTitle = \"";
result += ArticleMaker::tr( "Expand article" ).toUtf8().constData();
result += "\";\n"
"const gdCollapseArticleTitle = \"";
result += ArticleMaker::tr( "Collapse article" ).toUtf8().constData();
result += "\";\n"
"</script>"
"<script src='qrc:///scripts/blocking.js'></script>"
"\n";
}
class CssAppender
{
public:
/// @param needPageBackgroundColor_ whether findPageBackgroundColor() will be called.
explicit CssAppender( string & result_, bool needPageBackgroundColor_ ):
result( result_ ), needPageBackgroundColor( needPageBackgroundColor_ ), isPrintMedia( false )
{}
/// Style sheets appended after a call to this function apply only while printing.
/// @note: style sheets with media="print" are appended after uncoditional style sheets,
/// because print-only CSS needs higher priority to override the style used for printing.
void startPrintMedia()
{ isPrintMedia = true; }
void appendFile( QString const & fileName )
{
if( !QFileInfo( fileName ).isFile() )
return;
#ifndef USE_QTWEBKIT
// We are not looking for printed background color.
if( needPageBackgroundColor && !isPrintMedia )
cssFiles.push_back( fileName );
#endif
result += "<link href=\"";
result += Html::escape( localFileNameToHtml( fileName ) );
result += "\" rel=\"stylesheet\" media=\"";
result += isPrintMedia ? "print" : "all";
result += "\" />\n";
}
#ifndef USE_QTWEBKIT
static constexpr auto getPageBackgroundColorPropertyName()
{ return QLatin1String( pageBackgroundColorPropertyName, pageBackgroundColorPropertyNameSize ); }
/// @return The page background color or an empty string if
/// the background color could not be found in the style sheets.
/// @warning This function parses the style sheets that have been appended to @a result. Therefore it
/// must be called after appending all style sheets that may override the page background color.
string findPageBackgroundColor( vector< QString > & unspecifiedColorFileNames ) const
{
Q_ASSERT( needPageBackgroundColor );
string backgroundColor;
// Iterate in the reverse order because the CSS code lower in the page overrides.
std::find_if( cssFiles.crbegin(), cssFiles.crend(),
[ &backgroundColor, &unspecifiedColorFileNames ]( QString const & fileName ) {
QFile cssFile( fileName );
if( !cssFile.open( QIODevice::ReadOnly | QIODevice::Text ) )
{
gdWarning( "Couldn't open CSS file \"%s\" for reading: %s (%d)", qUtf8Printable( fileName ),
qUtf8Printable( cssFile.errorString() ), static_cast< int >( cssFile.error() ) );
return false;
}
if( !findPageBackgroundColor( cssFile, backgroundColor ) )
unspecifiedColorFileNames.push_back( fileName );
// Empty backgroundColor means that this CSS file does not override page
// background color, in which case we look for it in the remaining CSS files.
return !backgroundColor.empty();
} );
return backgroundColor;
}
#endif // USE_QTWEBKIT
private:
static std::string localFileNameToHtml( QString const & fileName )
{
string result;
if( fileName.startsWith( QLatin1String( ":/" ) ) )
{
// Replace the local file resource prefix ":/" with "qrc:///" for the web page.
result = "qrc:///";
result += fileName.toUtf8().constData() + 2;
}
else
{
// fileName must be a local filesystem path => convert it into a file URL.
#ifdef Q_OS_WIN32
result = "file:///";
#else
result = "file://";
#endif
result += fileName.toUtf8().constData();
}
return result;
}
string & result;
bool const needPageBackgroundColor;
bool isPrintMedia;
#ifndef USE_QTWEBKIT
static constexpr char pageBackgroundColorPropertyName[] = "--gd-page-background-color";
// - 1 accounts for the terminating null character.
static constexpr std::size_t pageBackgroundColorPropertyNameSize = sizeof( pageBackgroundColorPropertyName )
/ sizeof( char ) - 1;
vector< QString > cssFiles;
/// Finds the page background color in @p cssFile.
/// First looks for the page background color CSS property. If the property specification
/// is missing, falls back to a slower search of the background color of the <body> element.
/// @param[out] backgroundColor is set to the page background color or to an empty string
/// if the CSS file does not override page background color.
/// @return true if a valid page background color CSS property specification was found, false otherwise.
static bool findPageBackgroundColor( QFile & cssFile, string & backgroundColor )
{
Q_ASSERT( cssFile.isOpen() );
// TODO: remove the regex fallback, including this variable, all its uses and findBodyBackgroundColor(),
// once users have had some time to add the page background color property into their article style files.
string cssCode;
// At the time of writing, each line in built-in article style files fits into maxReasonableLineLength.
constexpr int maxReasonableLineLength = 212;
QVarLengthArray< char, maxReasonableLineLength > line( maxReasonableLineLength );
int offset = 0;
while( !cssFile.atEnd() )
{
int const maxSize = line.size() - offset;
int bytesRead = cssFile.readLine( line.data() + offset, maxSize );
if( bytesRead == -1 )
{
gdWarning( "Error while reading CSS file \"%s\": %s (%d)", qUtf8Printable( cssFile.fileName() ),
qUtf8Printable( cssFile.errorString() ), static_cast< int >( cssFile.error() ) );
break;
}
Q_ASSERT( bytesRead >= 0 );
Q_ASSERT( bytesRead < maxSize ); // QIODevice::readLine() reads up to a maximum of maxSize - 1 bytes.
if( bytesRead == maxSize - 1 && line[ line.size() - 2 ] != '\n' && !cssFile.atEnd() )
{
// This line is longer than line.size() => increase the buffer size until the entire line fits in.
Q_ASSERT( line.back() == 0 ); // A terminating '\0' byte is always appended to data.
offset = line.size() - 1; // Overwrite the terminating '\0' character during the next iteration.
line.resize( line.size() * 2 );
continue;
}
bytesRead += offset;
Q_ASSERT( bytesRead < line.size() );
Q_ASSERT( line[ bytesRead ] == 0 ); // A terminating '\0' byte is always appended to data.
// We require the property and its value to be on the same line to be able to process the CSS file line by line.
if( findPageBackgroundColorProperty( line.data(), line.data() + bytesRead, backgroundColor ) )
return true;
offset = 0;
cssCode.append( line.data(), bytesRead );
}
gdWarning( "Page background color specification is missing from CSS file \"%s\"",
qUtf8Printable( cssFile.fileName() ) );
backgroundColor = findBodyBackgroundColor( cssCode.data(), cssCode.data() + cssCode.size() );
return false;
}
static bool isSpace( char ch )
{
// The behavior of std::isspace() is undefined if the value of ch
// is not representable as unsigned char and is not equal to EOF.
return std::isspace( static_cast< unsigned char >( ch ) );
}
/// Finds the page background color CSS property in [@p first, @p last).
/// @param[out] backgroundColor is set to the page background color if the CSS property is found, unchanged otherwise.
/// @note @p backgroundColor set to an empty string means that the CSS file does not override page background color.
/// @return true if a valid page background color CSS property specification was found, false otherwise.
static bool findPageBackgroundColorProperty( char const * first, char const * last, string & backgroundColor )
{
char const * it = std::search( first, last, pageBackgroundColorPropertyName,
pageBackgroundColorPropertyName + pageBackgroundColorPropertyNameSize );
if( it == last )
return false;
it += pageBackgroundColorPropertyNameSize;
Q_ASSERT( it <= last );
auto const skipWhitespace = [ &it, last ] {
it = std::find_if_not( it, last, isSpace );
};
skipWhitespace();
if( it == last || *it != ':' )
{
gdWarning( "Missing colon after %s CSS property name. Ignoring this malformed specification.",
pageBackgroundColorPropertyName );
return false;
}
++it;
skipWhitespace();
char const * const colorBegin = it;
auto const isCssPropertyValueEnd = []( char ch ) {
return ch == ';' || ch == '}' || isSpace( ch );
};
it = std::find_if( it, last, isCssPropertyValueEnd );
backgroundColor.assign( colorBegin, it );
return true;
}
/// Finds the background color of the <body> element in [@p first, @p last).
/// @return the background color or an empty string if it could not be found.
static string findBodyBackgroundColor( char const * first, char const * last )
{
// This regular expression is simple and efficient. But the result is not always accurate:
// 1. The first word after "background:" is considered to be the color, even though this first word could
// be something else, e.g. "border-box".
// 2. The code inside CSS comments (/*comment*/) is matched too, not skipped.
// Built-in and user-defined style sheets must take this simplified matching into account.
// On the bright side, the user can easily override the background color matched here by adding a comment
// like /* body{ background:#abcdef } */ at the end of the user-defined article-style.css file.
static std::regex const backgroundRegex( R"(\bbody\s*\{[^}]*\bbackground(?:|-color)\s*:\s*([^\s;}]+))",
// CSS code is case-insensitive => regex::icase.
// The regex object is reused (static) and the CSS code can be large => regex::optimize.
std::regex::icase | std::regex::optimize );
// Iterate over all matches and return the last one, because the CSS code lower in the page overrides.
string::const_iterator::difference_type position = -1, length;
for( std::cregex_iterator it( first, last, backgroundRegex ), end; it != end; ++it )
{
Q_ASSERT( it->size() == 2 );
position = it->position( 1 );
Q_ASSERT( position >= 0 );
length = it->length( 1 );
Q_ASSERT( length > 0 );
Q_ASSERT( first + position + length <= last );
}
if( position == -1 )
return {};
return string( first + position, first + position + length );
}
#endif
};
#ifndef USE_QTWEBKIT
constexpr char CssAppender::pageBackgroundColorPropertyName[];
QString wrapInHtmlCodeElement( QLatin1String text )
{
return QLatin1String( "<code>%1</code>" ).arg( text );
}
QString bodyElementHtmlCode()
{
return wrapInHtmlCodeElement( QLatin1String{ "<body>" } );
}
QString htmlElementHtmlCode()
{
return wrapInHtmlCodeElement( QLatin1String{ "<html>" } );
}
QString propertyNameHtmlCode()
{
return wrapInHtmlCodeElement( CssAppender::getPageBackgroundColorPropertyName() );
}
QString missingSpecificationWarningMessage( vector< QString > const & unspecifiedColorFileNames )
{
Q_ASSERT( !unspecifiedColorFileNames.empty() );
QString message = ArticleMaker::tr( "<p>Page background color specification is missing from the following CSS files:"
"</p>" ) + QLatin1String( "<pre>" );
for( auto const & fileName : unspecifiedColorFileNames )
{
message += QLatin1String( "<p style='margin: 0px;'>" );
message += fileName;
message += QLatin1String( "</p>" );
}
message += QLatin1String( "</pre>" );
message += ArticleMaker::tr( "<p>Please insert a page background color specification at the top (or close to the "
"top) of each of these files. For example:</p>" );
// Recommend to define the page background color custom property on the :root pseudo-class to allow using it globally
// across the CSS file in the future. Do not recommend actually using the custom property value with
// `var(--gd-page-background-color)` for now. Custom CSS properties are not supported by Qt 5 WebKit, and it is
// important to maintain style sheet compatibility with the Qt WebKit version of GoldenDict while it is widely used.
message += QLatin1String( "<pre>:root\n{\n %1: COLOR;\n}</pre>" )
.arg( CssAppender::getPageBackgroundColorPropertyName() );
message += ArticleMaker::tr( "<p>Replace %1 with the actual page background color specified in the CSS file, that is "
"%2 or %3 background color. If the CSS file does not specify the page background color, "
"replace %1 with an empty string (without quotes).</p>"
"<p>Incorrect or missing page background color specification may cause article page "
"background flashes.</p>"
"<p>Supported page background color specification format is strict: quotes are not "
"allowed, the property name %4 and its value must be on the same line. On the other "
"hand, the surrounding declaration block does not matter to GoldenDict. The property "
"specification can just as well be inside a CSS comment instead of the %5 pseudo-class "
"block.</p>" )
.arg( wrapInHtmlCodeElement( QLatin1String{ "COLOR" } ), bodyElementHtmlCode(), htmlElementHtmlCode(),
propertyNameHtmlCode(), wrapInHtmlCodeElement( QLatin1String{ ":root" } ) );
return message;
}
QString invalidColorWarningMessage( QString const & pageBackgroundColor )
{
return ArticleMaker::tr( "<p>Invalid page background color is specified in the article style sheets:</p>" )
+ QLatin1String( "<pre>%1</pre>" ).arg( pageBackgroundColor )
+ ArticleMaker::tr( "<p>Set %1 to the actual page background color specified in the CSS file, that is %2 or "
"%3 background color. If the CSS file does not specify the page background color, set %1 "
"to an empty string (without quotes).</p>"
"<p>Supported color value formats:</p><ul>"
"<li>#RGB (each of R, G, and B is a single hex digit)</li>"
"<li>#RRGGBB</li>"
"<li>#AARRGGBB</li>"
"<li>#RRRGGGBBB</li>"
"<li>#RRRRGGGGBBBB</li>"
"<li>A name from the list of colors defined in the list of <a href=\""
"https://www.w3.org/TR/SVG11/types.html#ColorKeywords\">SVG color keyword names</a> "
"provided by the World Wide Web Consortium; for example, %4 or %5.</li>"
"<li>%6 - representing the absence of a color.</li></ul>" )
.arg( propertyNameHtmlCode(), bodyElementHtmlCode(), htmlElementHtmlCode(),
wrapInHtmlCodeElement( QLatin1String{ "steelblue" } ),
wrapInHtmlCodeElement( QLatin1String{ "gainsboro" } ),
wrapInHtmlCodeElement( QLatin1String{ "transparent" } ) );
}
#endif // USE_QTWEBKIT
} // unnamed namespace
ArticleMaker::ArticleMaker( vector< sptr< Dictionary::Class > > const & dictionaries_,
vector< Instances::Group > const & groups_,
QString const & displayStyle_,
QString const & addonStyle_,
QWidget * dialogParent_ ):
dictionaries( dictionaries_ ),
groups( groups_ ),
displayStyle( displayStyle_ ),
addonStyle( addonStyle_ ),
#ifndef USE_QTWEBKIT
dialogParent( dialogParent_ ),
#endif
needExpandOptionalParts( true )
, collapseBigArticles( true )
, articleLimitSize( 500 )
{
Q_UNUSED( dialogParent_ )
}
void ArticleMaker::setDisplayStyle( QString const & st, QString const & adst )
{
displayStyle = st;
addonStyle = adst;
}
#ifndef USE_QTWEBKIT
QColor ArticleMaker::colorFromString( string const & pageBackgroundColor ) const
{
if( pageBackgroundColor.empty() )
{
Q_ASSERT_X( false, Q_FUNC_INFO, "The default built-in style sheet :/article-style.css is unconditionally appended "
"and specifies a valid page background color, which is parsed correctly." );
gdWarning( "Couldn't find the page background color in the article style sheets." );
return QColor();
}
auto const pageBackgroundColorQString = QString::fromUtf8( pageBackgroundColor.c_str() );
QColor color( pageBackgroundColorQString );
if( !color.isValid() )
{
gdWarning( "Found invalid page background color in the article style sheets: \"%s\"", pageBackgroundColor.c_str() );
if( !hasShownBackgroundColorWarningMessage )
{
hasShownBackgroundColorWarningMessage = true;
QMessageBox::warning( dialogParent, "GoldenDict", invalidColorWarningMessage( pageBackgroundColorQString ) );
}
return QColor();
}
GD_DPRINTF( "Found page background color in the article style sheets: \"%s\" = %s\n", pageBackgroundColor.c_str(),
// Print the result of QColor's nontrivial parsing of pageBackgroundColor string.
// Print the alpha component only if the color is not fully opaque.
qPrintable( color.name( color.alpha() == 255 ? QColor::HexRgb : QColor::HexArgb ) ) );
return color;
}
#endif
void ArticleMaker::appendCss( string & result, bool expandOptionalParts, QColor * pageBackgroundColor ) const
{
CssAppender cssAppender( result, static_cast< bool >( pageBackgroundColor ) );
cssAppender.appendFile( ":/article-style.css" );
if( !displayStyle.isEmpty() )
cssAppender.appendFile( QString( ":/article-style-st-%1.css" ).arg( displayStyle ) );
cssAppender.appendFile( Config::getUserCssFileName() );
if( !addonStyle.isEmpty() )
cssAppender.appendFile( Config::getStylesDir() + addonStyle + QDir::separator() + "article-style.css" );
// Turn on/off expanding of article optional parts
if( expandOptionalParts )
cssAppender.appendFile( ":/article-style-expand-optional-parts.css" );
cssAppender.startPrintMedia();
cssAppender.appendFile( ":/article-style-print.css" );
cssAppender.appendFile( Config::getUserCssPrintFileName() );
if( !addonStyle.isEmpty() )
cssAppender.appendFile( Config::getStylesDir() + addonStyle + QDir::separator() + "article-style-print.css" );
#ifdef USE_QTWEBKIT
Q_ASSERT( !pageBackgroundColor );
#else
if( pageBackgroundColor )
{
vector< QString > unspecifiedColorFileNames;
auto const backgroundColorString = cssAppender.findPageBackgroundColor( unspecifiedColorFileNames );
if( !unspecifiedColorFileNames.empty() && !hasShownBackgroundColorWarningMessage )
{
hasShownBackgroundColorWarningMessage = true;
QMessageBox::warning( dialogParent, "GoldenDict",
missingSpecificationWarningMessage( unspecifiedColorFileNames ) );
}
*pageBackgroundColor = colorFromString( backgroundColorString );
}
#endif
}
std::string ArticleMaker::makeHtmlHeader( QString const & word,
QString const & icon,
bool expandOptionalParts,
QColor * pageBackgroundColor ) const
{
string result =
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
"<html><head>"
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">";
appendScripts( result );
appendCss( result, expandOptionalParts, pageBackgroundColor );
result += "<title>" + Html::escape( Utf8::encode( gd::toWString( word ) ) ) + "</title>";
// This doesn't seem to be much of influence right now, but we'll keep
// it anyway.
if ( icon.size() )
result += "<link rel=\"icon\" type=\"image/png\" href=\"qrc:///flags/" + Html::escape( icon.toUtf8().data() ) + "\" />\n";
result += "</head><body"
#ifndef USE_QTWEBKIT
// Qt WebEngine API does not provide a way to check whether a mouse click occurs on a page
// proper or on its scrollbar. We are only interested in clicks on the page contents
// within <body>. Listen to such mouse events and send messages from JavaScript to C++.
" onMouseDown='gdBodyMouseDown(event);'"
" onMouseUp='gdBodyMouseUp(event);'"
#endif
">";
return result;
}
std::string ArticleMaker::makeNotFoundBody( QString const & word,
QString const & group )
{
string result( "<div class=\"gdnotfound\"><p>" );
QString str( word );
Folding::prepareToEmbedRTL( str );
if ( word.size() )
result += tr( "No translation for <b>%1</b> was found in group <b>%2</b>." ).
arg( QString::fromUtf8( Html::escape( str.toUtf8().data() ).c_str() ) ).
arg( QString::fromUtf8( Html::escape( group.toUtf8().data() ).c_str() ) ).
toUtf8().data();
else
result += tr( "No translation was found in group <b>%1</b>." ).
arg( QString::fromUtf8( Html::escape( group.toUtf8().data() ).c_str() ) ).
toUtf8().data();
result += "</p></div>";
return result;
}
sptr< Dictionary::DataRequest > ArticleMaker::makeDefinitionFor(
Config::InputPhrase const & phrase, unsigned groupId,
QMap< QString, QString > const & contexts,
QSet< QString > const & mutedDicts,
QStringList const & dictIDs , bool ignoreDiacritics ) const
{
if( !dictIDs.isEmpty() )
{
QStringList ids = dictIDs;
std::vector< sptr< Dictionary::Class > > ftsDicts;
// Find dictionaries by ID's
for( unsigned x = 0; x < dictionaries.size(); x++ )
{
for( QStringList::Iterator it = ids.begin(); it != ids.end(); ++it )
{
if( *it == QString::fromStdString( dictionaries[ x ]->getId() ) )
{
ftsDicts.push_back( dictionaries[ x ] );
ids.erase( it );
break;
}
}
if( ids.isEmpty() )
break;
}
string header = makeHtmlHeader( phrase.phrase, QString(), true );
return new ArticleRequest( phrase, "",
contexts, ftsDicts, header,
-1, true );
}
if ( groupId == Instances::Group::HelpGroupId )
{
// This is a special group containing internal welcome/help pages
string result = makeHtmlHeader( phrase.phrase, QString(), needExpandOptionalParts );
if ( phrase.phrase == tr( "Welcome!" ) )
{
result += tr(
"<h3 align=\"center\">Welcome to <b>GoldenDict</b>!</h3>"
"<p>To start working with the program, first visit <b>Edit|Dictionaries</b> to add some directory paths where to search "
"for the dictionary files, set up various Wikipedia sites or other sources, adjust dictionary order or create dictionary groups."
"<p>And then you're ready to look up your words! You can do that in this window "
"by using a pane to the left, or you can <a href=\"Working with popup\">look up words from other active applications</a>. "
"<p>To customize program, check out the available preferences at <b>Edit|Preferences</b>. "
"All settings there have tooltips, be sure to read them if you are in doubt about anything."
"<p>Should you need further help, have any questions, "
"suggestions or just wonder what the others think, you are welcome at the program's <a href=\"http://goldendict.org/forum/\">forum</a>."
"<p>Check program's <a href=\"http://goldendict.org/\">website</a> for the updates. "
"<p>(c) 2008-2013 Konstantin Isakov. Licensed under GPLv3 or later."
).toUtf8().data();
}
else
if ( phrase.phrase == tr( "Working with popup" ) )
{
result += ( tr( "<h3 align=\"center\">Working with the popup</h3>"
"To look up words from other active applications, you would need to first activate the <i>\"Scan popup functionality\"</i> in <b>Preferences</b>, "
"and then enable it at any time either by triggering the 'Popup' icon above, or "
"by clicking the tray icon down below with your right mouse button and choosing so in the menu you've popped. " ) +
#ifdef Q_OS_WIN32
tr( "Then just stop the cursor over the word you want to look up in another application, "
"and a window would pop up which would describe it to you." )
#else
tr( "Then just select any word you want to look up in another application by your mouse "
"(double-click it or swipe it with mouse with the button pressed), "
"and a window would pop up which would describe the word to you." )
#endif
).toUtf8().data();
}
else
{
// Not found
return makeNotFoundTextFor( phrase.phrase, "help" );
}
result += "</body></html>";
sptr< Dictionary::DataRequestInstant > r = new Dictionary::DataRequestInstant( true );
r->getData().resize( result.size() );
memcpy( &( r->getData().front() ), result.data(), result.size() );
return r;
}
// Find the given group
Instances::Group const * activeGroup = 0;
for( unsigned x = 0; x < groups.size(); ++x )
if ( groups[ x ].id == groupId )
{
activeGroup = &groups[ x ];
break;
}
// If we've found a group, use its dictionaries; otherwise, use the global
// heap.
std::vector< sptr< Dictionary::Class > > const & activeDicts =
activeGroup ? activeGroup->dictionaries : dictionaries;
string header = makeHtmlHeader( phrase.phrase,
activeGroup && activeGroup->icon.size() ?
activeGroup->icon : QString(),
needExpandOptionalParts );
if ( mutedDicts.size() )
{
std::vector< sptr< Dictionary::Class > > unmutedDicts;
unmutedDicts.reserve( activeDicts.size() );
for( unsigned x = 0; x < activeDicts.size(); ++x )
if ( !mutedDicts.contains(
QString::fromStdString( activeDicts[ x ]->getId() ) ) )
unmutedDicts.push_back( activeDicts[ x ] );
return new ArticleRequest( phrase, activeGroup ? activeGroup->name : "",
contexts, unmutedDicts, header,
collapseBigArticles ? articleLimitSize : -1,
needExpandOptionalParts, ignoreDiacritics );
}
else
return new ArticleRequest( phrase, activeGroup ? activeGroup->name : "",
contexts, activeDicts, header,
collapseBigArticles ? articleLimitSize : -1,
needExpandOptionalParts, ignoreDiacritics );
}
sptr< Dictionary::DataRequest > ArticleMaker::makeNotFoundTextFor(
QString const & word, QString const & group ) const
{
string result = makeHtmlHeader( word, QString(), true ) + makeNotFoundBody( word, group ) +
"</body></html>";
sptr< Dictionary::DataRequestInstant > r = new Dictionary::DataRequestInstant( true );
r->getData().resize( result.size() );
memcpy( &( r->getData().front() ), result.data(), result.size() );
return r;
}
string ArticleMaker::makeBlankPageHtmlCode( QColor * pageBackgroundColor ) const
{
return makeHtmlHeader( tr( "(untitled)" ), QString(), true, pageBackgroundColor ) +
"</body></html>";
}
sptr< Dictionary::DataRequest > ArticleMaker::makeBlankPage() const
{
string const result = makeBlankPageHtmlCode();
sptr< Dictionary::DataRequestInstant > r =
new Dictionary::DataRequestInstant( true );
r->getData().resize( result.size() );
memcpy( &( r->getData().front() ), result.data(), result.size() );
return r;
}
sptr< Dictionary::DataRequest > ArticleMaker::makePicturePage( string const & url ) const
{
string result = makeHtmlHeader( tr( "(picture)" ), QString(), true )
+ "<a href=\"javascript: history.back();\">"
+ "<img src=\"" + url + "\" /></a>"
+ "</body></html>";
sptr< Dictionary::DataRequestInstant > r =
new Dictionary::DataRequestInstant( true );
r->getData().resize( result.size() );
memcpy( &( r->getData().front() ), result.data(), result.size() );
return r;
}
void ArticleMaker::setExpandOptionalParts( bool expand )
{
needExpandOptionalParts = expand;
}
void ArticleMaker::setCollapseParameters( bool autoCollapse, int articleSize )
{
collapseBigArticles = autoCollapse;
articleLimitSize = articleSize;
}
bool ArticleMaker::adjustFilePath( QString & fileName )
{
QFileInfo info( fileName );
if( !info.isFile() )
{
QString dir = Config::getConfigDir();
dir.chop( 1 );
info.setFile( dir + fileName);
if( info.isFile() )
{
fileName = info.canonicalFilePath();
return true;
}
}
return false;
}
//////// ArticleRequest
ArticleRequest::ArticleRequest(
Config::InputPhrase const & phrase, QString const & group_,
QMap< QString, QString > const & contexts_,
vector< sptr< Dictionary::Class > > const & activeDicts_,
string const & header,
int sizeLimit, bool needExpandOptionalParts_, bool ignoreDiacritics_ ):
word( phrase.phrase ), group( group_ ), contexts( contexts_ ),
activeDicts( activeDicts_ ),
altsDone( false ), bodyDone( false ), foundAnyDefinitions( false ),
closePrevSpan( false )
, articleSizeLimit( sizeLimit )
, needExpandOptionalParts( needExpandOptionalParts_ )
, ignoreDiacritics( ignoreDiacritics_ )
{
if ( !phrase.punctuationSuffix.isEmpty() )
alts.insert( gd::toWString( phrase.phraseWithSuffix() ) );
// No need to lock dataMutex on construction
hasAnyData = true;
data.resize( header.size() );
memcpy( &data.front(), header.data(), header.size() );
// Accumulate main forms
for( unsigned x = 0; x < activeDicts.size(); ++x )
{
sptr< Dictionary::WordSearchRequest > s = activeDicts[ x ]->findHeadwordsForSynonym( gd::toWString( word ) );
connect( s.get(), SIGNAL( finished() ),
this, SLOT( altSearchFinished() ), Qt::QueuedConnection );
altSearches.push_back( s );
}
altSearchFinished(); // Handle any ones which have already finished
}
void ArticleRequest::altSearchFinished()
{
if ( altsDone )
return;
// Check every request for finishing
for( list< sptr< Dictionary::WordSearchRequest > >::iterator i =
altSearches.begin(); i != altSearches.end(); )
{
if ( (*i)->isFinished() )
{
// This one's finished
for( size_t count = (*i)->matchesCount(), x = 0; x < count; ++x )
alts.insert( (**i)[ x ].word );
altSearches.erase( i++ );
}
else
++i;
}
if ( altSearches.empty() )
{
#ifdef QT_DEBUG
qDebug( "alts finished\n" );
#endif
// They all've finished! Now we can look up bodies
altsDone = true; // So any pending signals in queued mode won't mess us up
vector< wstring > altsVector( alts.begin(), alts.end() );
#ifdef QT_DEBUG
for( unsigned x = 0; x < altsVector.size(); ++x )
{
qDebug() << "Alt:" << gd::toQString( altsVector[ x ] );
}
#endif
wstring wordStd = gd::toWString( word );
if( activeDicts.size() <= 1 )
articleSizeLimit = -1; // Don't collapse article if only one dictionary presented
for( unsigned x = 0; x < activeDicts.size(); ++x )
{
try
{
sptr< Dictionary::DataRequest > r =
activeDicts[ x ]->getArticle( wordStd, altsVector,
gd::toWString( contexts.value( QString::fromStdString( activeDicts[ x ]->getId() ) ) ),
ignoreDiacritics );
connect( r.get(), SIGNAL( finished() ),
this, SLOT( bodyFinished() ), Qt::QueuedConnection );
bodyRequests.push_back( r );
}
catch( std::exception & e )
{
gdWarning( "getArticle request error (%s) in \"%s\"\n",
e.what(), activeDicts[ x ]->getName().c_str() );
}
}
bodyFinished(); // Handle any ones which have already finished
}
}
int ArticleRequest::findEndOfCloseDiv( const QString &str, int pos )
{
for( ; ; )
{
int n1 = str.indexOf( "</div>", pos );
if( n1 <= 0 )
return n1;
int n2 = str.indexOf( "<div ", pos );
if( n2 <= 0 || n2 > n1 )
return n1 + 6;
pos = findEndOfCloseDiv( str, n2 + 1 );
if( pos <= 0 )
return pos;
}
}
static void appendGdMakeArticleActiveOn( string & result, char const * jsEvent, string const & dictionaryId )
{
result += " on";
result += jsEvent;
result += "=\"gdMakeArticleActive('";
result += dictionaryId;
result += "');\"";
}
void ArticleRequest::bodyFinished()
{
if ( bodyDone )
return;
GD_DPRINTF( "some body finished\n" );
bool wasUpdated = false;
while ( bodyRequests.size() )
{
// Since requests should go in order, check the first one first
if ( bodyRequests.front()->isFinished() )
{
// Good
GD_DPRINTF( "one finished.\n" );
Dictionary::DataRequest & req = *bodyRequests.front();
QString errorString = req.getErrorString();
if ( req.dataSize() >= 0 || errorString.size() )
{
sptr< Dictionary::Class > const & activeDict =
activeDicts[ activeDicts.size() - bodyRequests.size() ];
string dictId = activeDict->getId();
string head;
string gdFrom = "gdfrom-" + Html::escape( dictId );
if ( closePrevSpan )
{
head += "</div></div><div style=\"clear:both;\"></div><span class=\"gdarticleseparator\"></span>";
}
// else: this is the first article
bool collapse = false;
if( articleSizeLimit >= 0 )
{
try
{
Mutex::Lock _( dataMutex );
QString text = QString::fromUtf8( req.getFullData().data(), req.getFullData().size() );
if( !needExpandOptionalParts )
{
// Strip DSL optional parts
int pos = 0;
for( ; ; )
{
pos = text.indexOf( "<div class=\"dsl_opt\"" );
if( pos > 0 )
{
int endPos = findEndOfCloseDiv( text, pos + 1 );
if( endPos > pos)
text.remove( pos, endPos - pos );
else
break;
}
else
break;
}
}
int size = QTextDocumentFragment::fromHtml( text ).toPlainText().length();
if( size > articleSizeLimit )
collapse = true;
}
catch(...)
{
}
}
string jsVal = Html::escapeForJavaScript( dictId );
head += string( "<div class=\"gdarticle" ) +
#ifdef USE_QTWEBKIT
// gdCurrentArticleLoaded() initializes " gdactivearticle" in the Qt WebEngine version.
( closePrevSpan ? "" : " gdactivearticle" ) +
#endif
( collapse ? " gdcollapsedarticle" : "" ) +
"\" id=\"" + gdFrom + '"';
// Make the article active on left, middle or right mouse button click.
appendGdMakeArticleActiveOn( head, "click", jsVal );
// A right mouse button click triggers only "contextmenu" JavaScript event.
appendGdMakeArticleActiveOn( head, "contextmenu", jsVal );
// In the Qt WebKit version both a left and a middle mouse button click triggers "click" JavaScript event.
// In the Qt WebEngine version a left mouse button click triggers "click", a middle - "auxclick" event.
#ifndef USE_QTWEBKIT
appendGdMakeArticleActiveOn( head, "auxclick", jsVal );
#endif