Merge branch 'staged' into dev
Some checks are pending
Release All / build_macOS (macos-13, 6.6.3) (push) Waiting to run
Release All / build_macOS (macos-13, 6.7.2) (push) Waiting to run
Release All / build_macOS (macos-14, 6.6.3) (push) Waiting to run
Release All / build_macOS (macos-14, 6.7.2) (push) Waiting to run
Release All / build_Windows (windows-2022, 6.6.3) (push) Waiting to run
Release All / build_Windows (windows-2022, 6.7.2) (push) Waiting to run
Release All / generate_other_staffs (push) Waiting to run
Release All / publish (push) Blocked by required conditions

This commit is contained in:
YiFang Xiao 2024-12-17 20:32:37 +08:00
commit 7f62fc87c4
18 changed files with 204 additions and 81 deletions

View file

@ -56,7 +56,7 @@ jobs:
brew update brew update
- name: Install dependencies - name: Install dependencies
run: | run: |
brew install \ brew install --force --overwrite \
ninja \ ninja \
opencc \ opencc \
ffmpeg \ ffmpeg \
@ -68,7 +68,7 @@ jobs:
hunspell \ hunspell \
xapian \ xapian \
libzim \ libzim \
qt qt || true
- name: Install eb - name: Install eb
run: | run: |
wget https://github.com/mistydemeo/eb/releases/download/v4.4.3/eb-4.4.3.tar.bz2 wget https://github.com/mistydemeo/eb/releases/download/v4.4.3/eb-4.4.3.tar.bz2

View file

@ -31,7 +31,7 @@ jobs:
brew update brew update
- name: Install dependencies - name: Install dependencies
run: | run: |
brew install \ brew install --force --overwrite \
bzip2 \ bzip2 \
create-dmg \ create-dmg \
hunspell \ hunspell \
@ -42,7 +42,7 @@ jobs:
lzip \ lzip \
ninja \ ninja \
opencc \ opencc \
xapian xapian || true
- name: Install eb - name: Install eb
run: | run: |
git clone https://github.com/xiaoyifang/eb.git git clone https://github.com/xiaoyifang/eb.git

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View file

@ -0,0 +1,32 @@
#include "dictionary_icon_name.hh"
#include <QMutexLocker>
QString Icons::DictionaryIconName::getIconName( const QString & dictionaryName )
{
if ( dictionaryName.isEmpty() ) {
return {};
}
QMutexLocker _( &_mutex );
auto it = _dictionaryIconNames.contains( dictionaryName );
if ( it ) {
return _dictionaryIconNames.value( dictionaryName );
}
//get the first character of the dictionary name
QString name = dictionaryName.at( 0 ).toUpper();
auto it1 = _iconDictionaryNames.contains( name );
std::vector< QString > vector = {};
if ( it1 ) {
vector = _iconDictionaryNames.value( name );
vector.emplace_back( dictionaryName );
}
else {
vector.emplace_back( dictionaryName );
_iconDictionaryNames.insert( name, vector );
}
name = name + QString::number( vector.size() );
_dictionaryIconNames.insert( dictionaryName, name );
return name;
}

View file

@ -0,0 +1,24 @@
#pragma once
#include <string>
#include <QMap>
#include <vector>
#include <mutex>
#include <QString>
#include <QMutex>
namespace Icons {
//use dictionary name's (first character + the order number) to represent the dictionary name in the icon image.
class DictionaryIconName
{
//map icon name to dictionary names;
QMap< QString, std::vector< QString > > _iconDictionaryNames;
//map dictionary name to icon name;
QMap< QString, QString > _dictionaryIconNames;
QMutex _mutex;
public:
QString getIconName( const QString & dictionaryName );
};
} // namespace Icons

View file

@ -155,8 +155,7 @@ std::u32string applyWhitespaceAndPunctOnly( std::u32string const & in )
bool isWhitespace( char32_t ch ) bool isWhitespace( char32_t ch )
{ {
//invisible character should be treated as whitespace as well. return QChar::isSpace( ch );
return QChar::isSpace( ch ) || !QChar::isPrint( ch );
} }
bool isWhitespaceOrPunct( char32_t ch ) bool isWhitespaceOrPunct( char32_t ch )

View file

@ -38,4 +38,22 @@ bool GlobalBroadcaster::existedInWhitelist( QString url ) const
{ {
return whitelist.contains( url ); return whitelist.contains( url );
} }
QString GlobalBroadcaster::getAbbrName( QString const & text )
{
if ( text.isEmpty() ) {
return {};
}
//remove whitespace,number,mark,puncuation,symbol
QString simplified = text;
simplified.remove(
QRegularExpression( R"([\p{Z}\p{N}\p{M}\p{P}\p{S}])", QRegularExpression::UseUnicodePropertiesOption ) );
if ( simplified.isEmpty() ) {
return {};
}
return _icon_names.getIconName( simplified );
}
// namespace global // namespace global

View file

@ -5,6 +5,7 @@
#include "config.hh" #include "config.hh"
#include "pronounceengine.hh" #include "pronounceengine.hh"
#include <QCache> #include <QCache>
#include "dictionary_icon_name.hh"
struct ActiveDictIds struct ActiveDictIds
{ {
@ -25,6 +26,7 @@ class GlobalBroadcaster: public QObject
Config::Preferences * preference; Config::Preferences * preference;
QSet< QString > whitelist; QSet< QString > whitelist;
Icons::DictionaryIconName _icon_names;
public: public:
void setPreference( Config::Preferences * _pre ); void setPreference( Config::Preferences * _pre );
@ -40,6 +42,7 @@ public:
QMap< QString, QSet< QString > > folderFavoritesMap; QMap< QString, QSet< QString > > folderFavoritesMap;
QMap< unsigned, QString > groupFolderMap; QMap< unsigned, QString > groupFolderMap;
PronounceEngine pronounce_engine; PronounceEngine pronounce_engine;
QString getAbbrName( QString const & text );
signals: signals:
void dictionaryChanges( ActiveDictIds ad ); void dictionaryChanges( ActiveDictIds ad );
void dictionaryClear( ActiveDictIds ad ); void dictionaryClear( ActiveDictIds ad );

View file

@ -71,8 +71,8 @@ const static QRegularExpression accentMark( R"(\p{M})", QRegularExpression::UseU
//contain unicode space mark,invisible, and punctuation //contain unicode space mark,invisible, and punctuation
const static QRegularExpression markPuncSpace( R"([\p{M}\p{Z}\p{C}\p{P}])", const static QRegularExpression markPuncSpace( R"([\p{M}\p{Z}\p{C}\p{P}])",
QRegularExpression::UseUnicodePropertiesOption ); QRegularExpression::UseUnicodePropertiesOption );
//contain unicode space and mark.invisible //contain unicode space and mark.
const static QRegularExpression markSpace( R"([\p{M}\p{Z}\p{C}])", QRegularExpression::UseUnicodePropertiesOption ); const static QRegularExpression markSpace( R"([\p{M}\p{Z}])", QRegularExpression::UseUnicodePropertiesOption );
const static QRegularExpression whiteSpace( "\\s+" ); const static QRegularExpression whiteSpace( "\\s+" );

View file

@ -39,6 +39,19 @@ inline QString rstrip( const QString & str )
return {}; return {};
} }
inline uint32_t leadingSpaceCount( const QString & str )
{
for ( int i = 0; i < str.size(); i++ ) {
if ( str.at( i ).isSpace() ) {
continue;
}
else {
return i;
}
}
return 0;
}
std::string c_string( const QString & str ); std::string c_string( const QString & str );
bool endsWithIgnoreCase( QByteArrayView str, QByteArrayView extension ); bool endsWithIgnoreCase( QByteArrayView str, QByteArrayView extension );
/** /**

View file

@ -19,6 +19,7 @@
#include <QRegularExpression> #include <QRegularExpression>
#include "utils.hh" #include "utils.hh"
#include "zipfile.hh" #include "zipfile.hh"
#include <array>
namespace Dictionary { namespace Dictionary {
@ -291,7 +292,7 @@ bool Class::loadIconFromFile( QString const & _filename, bool isFullName )
return false; return false;
} }
bool Class::loadIconFromText( QString iconUrl, QString const & text ) bool Class::loadIconFromText( const QString & iconUrl, QString const & text )
{ {
if ( text.isEmpty() ) { if ( text.isEmpty() ) {
return false; return false;
@ -308,7 +309,7 @@ bool Class::loadIconFromText( QString iconUrl, QString const & text )
painter.setCompositionMode( QPainter::CompositionMode_SourceAtop ); painter.setCompositionMode( QPainter::CompositionMode_SourceAtop );
QFont font = painter.font(); QFont font = painter.font();
//the text should be a little smaller than the icon //the orderNum should be a little smaller than the icon
font.setPixelSize( iconSize * 0.6 ); font.setPixelSize( iconSize * 0.6 );
font.setWeight( QFont::Bold ); font.setWeight( QFont::Bold );
painter.setFont( font ); painter.setFont( font );
@ -318,8 +319,21 @@ bool Class::loadIconFromText( QString iconUrl, QString const & text )
//select a single char. //select a single char.
auto abbrName = getAbbrName( text ); auto abbrName = getAbbrName( text );
painter.setPen( QColor( 4, 57, 108, 200 ) ); painter.setPen( intToFixedColor( qHash( abbrName ) ) );
painter.drawText( rectangle, Qt::AlignCenter, abbrName );
// Draw first character
painter.drawText( rectangle, Qt::AlignCenter, abbrName.at( 0 ) );
//the orderNum should be a little smaller than the icon
font.setPixelSize( iconSize * 0.4 );
QFontMetrics fm1( font );
const QString & orderNum = abbrName.mid( 1 );
int orderNumberWidth = fm1.horizontalAdvance( orderNum );
painter.setFont( font );
painter.drawText( rectangle.x() + rectangle.width() - orderNumberWidth * 1.2,
rectangle.y() + rectangle.height(),
orderNum );
painter.end(); painter.end();
@ -330,35 +344,30 @@ bool Class::loadIconFromText( QString iconUrl, QString const & text )
return false; return false;
} }
QColor Class::intToFixedColor( int index )
{
// Predefined list of colors
static const std::array colors = {
QColor( 255, 0, 0, 200 ), // Red
QColor( 4, 57, 108, 200 ), //Custom
QColor( 0, 255, 0, 200 ), // Green
QColor( 0, 0, 255, 200 ), // Blue
QColor( 255, 255, 0, 200 ), // Yellow
QColor( 0, 255, 255, 200 ), // Cyan
QColor( 255, 0, 255, 200 ), // Magenta
QColor( 192, 192, 192, 200 ), // Gray
QColor( 255, 165, 0, 200 ), // Orange
QColor( 128, 0, 128, 200 ), // Violet
QColor( 128, 128, 0, 200 ) // Olive
};
// Use modulo operation to ensure index is within the range of the color list
return colors[ index % colors.size() ];
}
QString Class::getAbbrName( QString const & text ) QString Class::getAbbrName( QString const & text )
{ {
if ( text.isEmpty() ) { return GlobalBroadcaster::instance()->getAbbrName( text );
return {};
}
//remove whitespace,number,mark,puncuation,symbol
QString simplified = text;
simplified.remove(
QRegularExpression( R"([\p{Z}\p{N}\p{M}\p{P}\p{S}])", QRegularExpression::UseUnicodePropertiesOption ) );
if ( simplified.isEmpty() ) {
return {};
}
int index = qHash( simplified ) % simplified.size();
QString abbrName;
if ( !Utils::isCJKChar( simplified.at( index ).unicode() ) ) {
// take two chars.
abbrName = simplified.mid( index, 2 );
if ( abbrName.size() == 1 ) {
//make up two characters.
abbrName = abbrName + simplified.at( 0 );
}
}
else {
abbrName = simplified.mid( index, 1 );
}
return abbrName;
} }
void Class::isolateCSS( QString & css, QString const & wrapperSelector ) void Class::isolateCSS( QString & css, QString const & wrapperSelector )

View file

@ -318,10 +318,10 @@ protected:
// Load icon from filename directly if isFullName == true // Load icon from filename directly if isFullName == true
// else treat filename as name without extension // else treat filename as name without extension
bool loadIconFromFile( QString const & filename, bool isFullName = false ); bool loadIconFromFile( QString const & filename, bool isFullName = false );
bool loadIconFromText( QString iconUrl, QString const & text ); bool loadIconFromText( const QString & iconUrl, QString const & text );
QString getAbbrName( QString const & text );
static QString getAbbrName( QString const & text );
static QColor intToFixedColor( int index );
/// Make css content usable only for articles from this dictionary /// Make css content usable only for articles from this dictionary
void isolateCSS( QString & css, QString const & wrapperSelector = QString() ); void isolateCSS( QString & css, QString const & wrapperSelector = QString() );

View file

@ -592,12 +592,49 @@ public:
cancel(); cancel();
} ); } );
connect( this, &DictServerArticleRequest::finishedArticle, this, [ this ]( QString articleText ) { connect( this, &DictServerArticleRequest::finishedArticle, this, [ this ]( QString _articleText ) {
if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) { if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) {
cancel(); cancel();
return; return;
} }
//modify the _articleText,remove extra lines[start with 15X etc.]
QList< QString > lines = _articleText.split( "\n", Qt::SkipEmptyParts );
QString resultStr;
// process the line
static QRegularExpression leadingRespCode( "^\\d{3} " );
uint32_t leadingSpaceCount = 0;
uint32_t firstLeadingSpaceCount = 0;
for ( const QString & line : lines ) {
//ignore 15X lines
if ( leadingRespCode.match( line ).hasMatch() ) {
continue;
}
// ignore dot(.),the end line character
if ( line.trimmed() == "." ) {
break;
}
auto lsc = Utils::leadingSpaceCount( line );
if ( firstLeadingSpaceCount == 0 && lsc > firstLeadingSpaceCount ) {
firstLeadingSpaceCount = lsc;
}
if ( lsc >= leadingSpaceCount && lsc > firstLeadingSpaceCount ) {
//extra space
resultStr.append( " " );
resultStr.append( line.trimmed() );
}
else {
resultStr.append( "\n" );
resultStr.append( line );
}
leadingSpaceCount = lsc;
}
static QRegularExpression phonetic( R"(\\([^\\]+)\\)", static QRegularExpression phonetic( R"(\\([^\\]+)\\)",
QRegularExpression::CaseInsensitiveOption ); // phonetics: \stuff\ ... QRegularExpression::CaseInsensitiveOption ); // phonetics: \stuff\ ...
static QRegularExpression divs_inside_phonetic( "</div([^>]*)><div([^>]*)>", static QRegularExpression divs_inside_phonetic( "</div([^>]*)><div([^>]*)>",
@ -610,26 +647,26 @@ public:
string articleStr; string articleStr;
if ( contentInHtml ) { if ( contentInHtml ) {
articleStr = articleText.toUtf8().data(); articleStr = resultStr.toUtf8().data();
} }
else { else {
articleStr = Html::preformat( articleText.toUtf8().data() ); articleStr = Html::preformat( resultStr.toUtf8().data() );
} }
articleText = QString::fromUtf8( articleStr.c_str(), articleStr.size() ); _articleText = QString::fromUtf8( articleStr.c_str(), articleStr.size() );
int pos; int pos;
if ( !contentInHtml ) { if ( !contentInHtml ) {
articleText = articleText.replace( refs, R"(<a href="gdlookup://localhost/\1">\1</a>)" ); _articleText = _articleText.replace( refs, R"(<a href="gdlookup://localhost/\1">\1</a>)" );
pos = 0; pos = 0;
QString articleNewText; QString articleNewText;
// Handle phonetics // Handle phonetics
QRegularExpressionMatchIterator it = phonetic.globalMatch( articleText ); QRegularExpressionMatchIterator it = phonetic.globalMatch( _articleText );
while ( it.hasNext() ) { while ( it.hasNext() ) {
QRegularExpressionMatch match = it.next(); QRegularExpressionMatch match = it.next();
articleNewText += articleText.mid( pos, match.capturedStart() - pos ); articleNewText += _articleText.mid( pos, match.capturedStart() - pos );
pos = match.capturedEnd(); pos = match.capturedEnd();
QString phonetic_text = match.captured( 1 ); QString phonetic_text = match.captured( 1 );
@ -638,18 +675,18 @@ public:
articleNewText += R"(<span class="dictd_phonetic">)" + phonetic_text + "</span>"; articleNewText += R"(<span class="dictd_phonetic">)" + phonetic_text + "</span>";
} }
if ( pos ) { if ( pos ) {
articleNewText += articleText.mid( pos ); articleNewText += _articleText.mid( pos );
articleText = articleNewText; _articleText = articleNewText;
articleNewText.clear(); articleNewText.clear();
} }
// Handle links // Handle links
pos = 0; pos = 0;
it = links.globalMatch( articleText ); it = links.globalMatch( _articleText );
while ( it.hasNext() ) { while ( it.hasNext() ) {
QRegularExpressionMatch match = it.next(); QRegularExpressionMatch match = it.next();
articleNewText += articleText.mid( pos, match.capturedStart() - pos ); articleNewText += _articleText.mid( pos, match.capturedStart() - pos );
pos = match.capturedEnd(); pos = match.capturedEnd();
QString link = match.captured( 1 ); QString link = match.captured( 1 );
@ -663,13 +700,13 @@ public:
articleNewText += newLink; articleNewText += newLink;
} }
if ( pos ) { if ( pos ) {
articleNewText += articleText.mid( pos ); articleNewText += _articleText.mid( pos );
articleText = articleNewText; _articleText = articleNewText;
articleNewText.clear(); articleNewText.clear();
} }
} }
articleData += string( "<div class=\"dictd_article\">" ) + articleText.toUtf8().data() + "<br></div>"; articleData += string( "<div class=\"dictd_article\">" ) + _articleText.toUtf8().data() + "<br></div>";
if ( !articleData.empty() ) { if ( !articleData.empty() ) {

View file

@ -877,8 +877,7 @@ QString & MdxDictionary::filterResource( QString & article )
void MdxDictionary::replaceLinks( QString & id, QString & article ) void MdxDictionary::replaceLinks( QString & id, QString & article )
{ {
QString articleNewText; QString articleNewText;
qsizetype linkPos = 0; int linkPos = 0;
QRegularExpressionMatchIterator it = RX::Mdx::allLinksRe.globalMatch( article ); QRegularExpressionMatchIterator it = RX::Mdx::allLinksRe.globalMatch( article );
while ( it.hasNext() ) { while ( it.hasNext() ) {
QRegularExpressionMatch allLinksMatch = it.next(); QRegularExpressionMatch allLinksMatch = it.next();
@ -954,14 +953,13 @@ void MdxDictionary::replaceLinks( QString & id, QString & article )
articleNewText += linkTxt; articleNewText += linkTxt;
match = RX::Mdx::closeScriptTagRe.match( article, linkPos ); match = RX::Mdx::closeScriptTagRe.match( article, linkPos );
if ( match.hasMatch() ) { if ( match.hasMatch() ) {
articleNewText += QString( QStringLiteral( "gdOnReady(()=>{%1});</script>" ) ) articleNewText += article.mid( linkPos, match.capturedEnd() - linkPos );
.arg( article.mid( linkPos, match.capturedStart() - linkPos ) );
linkPos = match.capturedEnd(); linkPos = match.capturedEnd();
} }
continue; continue;
} }
else { else {
//audio ,script,video ,html5 tags fall here. //audio ,video ,html5 tags fall here.
match = RX::Mdx::srcRe.match( linkTxt ); match = RX::Mdx::srcRe.match( linkTxt );
if ( match.hasMatch() ) { if ( match.hasMatch() ) {
QString newText; QString newText;
@ -973,15 +971,9 @@ void MdxDictionary::replaceLinks( QString & id, QString & article )
else { else {
scheme = "bres://"; scheme = "bres://";
} }
newText = newText =
match.captured( 1 ) + match.captured( 2 ) + scheme + id + "/" + match.captured( 3 ) + match.captured( 2 ); match.captured( 1 ) + match.captured( 2 ) + scheme + id + "/" + match.captured( 3 ) + match.captured( 2 );
//add defer to script tag
if ( linkType.compare( "script" ) == 0 ) {
newText = newText + " defer ";
}
newLink = linkTxt.replace( match.capturedStart(), match.capturedLength(), newText ); newLink = linkTxt.replace( match.capturedStart(), match.capturedLength(), newText );
} }
else { else {

View file

@ -275,14 +275,18 @@ quint32 LangCoder::guessId( const QString & lang )
std::pair< quint32, quint32 > LangCoder::findLangIdPairFromName( QString const & name ) std::pair< quint32, quint32 > LangCoder::findLangIdPairFromName( QString const & name )
{ {
static QRegularExpression reg( "(?=([a-z]{2,3})-([a-z]{2,3}))", QRegularExpression::CaseInsensitiveOption ); static QRegularExpression reg( "(^|[^a-z])((?<lang1>[a-z]{2,3})-(?<lang2>[a-z]{2,3}))($|[^a-z])",
QRegularExpression::CaseInsensitiveOption );
auto matches = reg.globalMatch( name ); auto matches = reg.globalMatch( name );
while ( matches.hasNext() ) { while ( matches.hasNext() ) {
auto m = matches.next(); auto m = matches.next();
if ( matches.hasNext() ) {
continue; // We use only the last match, skip previous ones
}
auto fromId = guessId( m.captured( 1 ).toLower() ); auto fromId = guessId( m.captured( "lang1" ).toLower() );
auto toId = guessId( m.captured( 2 ).toLower() ); auto toId = guessId( m.captured( "lang2" ).toLower() );
if ( code2Exists( intToCode2( fromId ) ) && code2Exists( intToCode2( toId ) ) ) { if ( code2Exists( intToCode2( fromId ) ) && code2Exists( intToCode2( toId ) ) ) {
return { fromId, toId }; return { fromId, toId };

View file

@ -1,11 +1,3 @@
function gdOnReady(func) {
if (document.readyState !== "loading") {
func();
} else {
document.addEventListener("DOMContentLoaded", func);
}
}
function gdMakeArticleActive(newId, noEvent) { function gdMakeArticleActive(newId, noEvent) {
const gdCurrentArticle = const gdCurrentArticle =
document.querySelector(".gdactivearticle").attributes.id; document.querySelector(".gdactivearticle").attributes.id;

View file

@ -4025,7 +4025,7 @@ void MainWindow::on_importFavorites_triggered()
QString fileName = QFileDialog::getOpenFileName( this, QString fileName = QFileDialog::getOpenFileName( this,
tr( "Import Favorites from file" ), tr( "Import Favorites from file" ),
importPath, importPath,
tr( "XML files (*.xml);;Txt files (*.txt);;All files (*.*)" ) ); tr( "Text and XML files (*.txt *.xml);;All files (*.*)" ) );
if ( fileName.size() == 0 ) { if ( fileName.size() == 0 ) {
return; return;
} }

View file

@ -10,9 +10,9 @@ Additionally, multiple strategies of automatic grouping are provided:
## Auto groups by dictionary language ## Auto groups by dictionary language
For formats like DSL, which has embedded language from / to metadata, GoldenDict will use the dictionary's built-in metadata. For formats like DSL, which has embedded language from / to metadata, GD will use the dictionary's built-in metadata.
For other formats, GoldenDict will try to extract languages from the dictionary's name or its file name by finding `{id}-{id}` pair. The `{id}` is 2 or 3 letters ISO 639 codes. For example, if a dictionary named `some name en-zh`, it will be automatically grouped into `en-zh`. For other formats, GD will try finding the last `{id}-{id}` pair delimited by non-alphabets in dictionary name or main file name to extract languages. The `{id}` is 2 or 3 letters ISO 639 codes. For example, if a dictionary named `some name en-zh`, it will be automatically grouped into `en-zh`.
Groups created in this method also include a context menu when right-click the group name, in which you can do additional dictionaries grouping by source or target language and combine dictionaries in more large groups. Groups created in this method also include a context menu when right-click the group name, in which you can do additional dictionaries grouping by source or target language and combine dictionaries in more large groups.