From 6c482ccb200d0dae22804589cb1bded9b629d0ce Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Wed, 22 Oct 2025 06:35:05 +0200 Subject: [PATCH 01/11] search and highlight text in view --- include/qlitehtmlbrowser/QLiteHtmlBrowser.h | 5 + src/QLiteHtmlBrowser.cpp | 15 ++ src/QLiteHtmlBrowserImpl.cpp | 27 +++ src/QLiteHtmlBrowserImpl.h | 4 + src/container_qt.cpp | 253 +++++++++++++++++++- src/container_qt.h | 30 ++- test/browser/testbrowser.cpp | 53 ++++ test/browser/testbrowser.h | 6 + 8 files changed, 391 insertions(+), 2 deletions(-) diff --git a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h index 2e57725..5a8f9e7 100644 --- a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h +++ b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h @@ -139,6 +139,11 @@ class QLITEHTMLBROWSER_EXPORT QLiteHtmlBrowser : public QWidget /// print document into paged paint device like a printer or pdf void print( QPagedPaintDevice* printer ) const; + /// search text in document + int searchText( const QString& ); + void nextSearchResult(); + void previousSearchResult(); + public Q_SLOTS: /// set URL to given url. The URL may be an url to local file, QtHelp, http etc. /// The URL could contain an anchor element. Currently parameters to URL like diff --git a/src/QLiteHtmlBrowser.cpp b/src/QLiteHtmlBrowser.cpp index 9e3adfb..cbd8eaf 100644 --- a/src/QLiteHtmlBrowser.cpp +++ b/src/QLiteHtmlBrowser.cpp @@ -172,3 +172,18 @@ void QLiteHtmlBrowser::print( QPagedPaintDevice* printer ) const mImpl->print( printer ); } } + +int QLiteHtmlBrowser::searchText( const QString& text ) +{ + return mImpl->searchText( text ); +} + +void QLiteHtmlBrowser::nextSearchResult() +{ + mImpl->nextSearchResult(); +} + +void QLiteHtmlBrowser::previousSearchResult() +{ + mImpl->previousSearchResult(); +} diff --git a/src/QLiteHtmlBrowserImpl.cpp b/src/QLiteHtmlBrowserImpl.cpp index ab7968d..b2c83e4 100644 --- a/src/QLiteHtmlBrowserImpl.cpp +++ b/src/QLiteHtmlBrowserImpl.cpp @@ -455,3 +455,30 @@ void QLiteHtmlBrowserImpl::print( QPagedPaintDevice* printer ) const mContainer->print( printer ); } } + +int QLiteHtmlBrowserImpl::searchText( const QString& phrase ) +{ + auto ret = 0; + if ( mContainer && !phrase.isEmpty() ) + { + ret = mContainer->searchText( phrase ); + } + + return ret; +} + +void QLiteHtmlBrowserImpl::nextSearchResult() +{ + if ( mContainer ) + { + mContainer->scrollToNextSearchResult(); + } +} + +void QLiteHtmlBrowserImpl::previousSearchResult() +{ + if ( mContainer ) + { + mContainer->scrollToPreviousSearchResult(); + } +} diff --git a/src/QLiteHtmlBrowserImpl.h b/src/QLiteHtmlBrowserImpl.h index d1b17f1..d8bac2b 100644 --- a/src/QLiteHtmlBrowserImpl.h +++ b/src/QLiteHtmlBrowserImpl.h @@ -68,6 +68,10 @@ class QLiteHtmlBrowserImpl : public QWidget const QString& caption() const; void print( QPagedPaintDevice* printer ) const; + int searchText( const QString& ); // search for given string and return number of found results + void nextSearchResult(); + void previousSearchResult(); + protected: void changeEvent( QEvent* ) override; void mousePressEvent( QMouseEvent* ) override; diff --git a/src/container_qt.cpp b/src/container_qt.cpp index d87cc28..798f887 100644 --- a/src/container_qt.cpp +++ b/src/container_qt.cpp @@ -30,6 +30,44 @@ static const auto CSS_GENERIC_FONT_TO_QFONT_STYLEHINT = QMap +#include +#include +#include +#include + +// // Textsuche durchführen (case-sensitive) +// std::string search_term = "Beispiel"; +// int result_count = container.search_text(doc, search_term, true); + +// std::cout << "Gefunden: " << result_count << " Vorkommen von '" +// << search_term << "'" << std::endl; + +// // Durch alle Ergebnisse iterieren +// const auto& all_results = container.get_all_results(); +// for (size_t i = 0; i < all_results.size(); ++i) { +// const auto& result = all_results[i]; +// std::cout << "Treffer " << (i + 1) << ":" << std::endl; +// std::cout << " Text: '" << result.matched_text << "'" << std::endl; +// std::cout << " Position: x=" << result.element_pos.x +// << ", y=" << result.element_pos.y << std::endl; +// std::cout << " Größe: " << result.element_pos.width +// << "x" << result.element_pos.height << std::endl; +// std::cout << " Offset: " << result.char_offset << std::endl; +// } + +// // Navigation durch Suchergebnisse +// container.next_search_result(); +// const TextSearchResult* current = container.get_current_result(); +// if (current) { +// std::cout << "\nAktueller Treffer an Position: (" +// << current->element_pos.x << ", " +// << current->element_pos.y << ")" << std::endl; +// } + +// return 0; +// } + container_qt::container_qt( QWidget* parent ) : QAbstractScrollArea( parent ) { @@ -123,7 +161,10 @@ void container_qt::paintEvent( QPaintEvent* event ) auto margins = viewport()->contentsMargins(); auto scroll_pos = -scrollBarPos(); - mDocument->draw( reinterpret_cast( &p ), margins.left() + scroll_pos.x(), margins.top() + scroll_pos.y(), &clipRect ); + auto hdc = reinterpret_cast( &p ); + + mDocument->draw( hdc, margins.left() + scroll_pos.x(), margins.top() + scroll_pos.y(), &clipRect ); + draw_highlights( hdc ); } } } @@ -1007,7 +1048,12 @@ void container_qt::mousePressEvent( QMouseEvent* e ) e->accept(); } else + { + auto elem = mDocument->get_over_element(); + qDebug() << elem->get_placement().x << elem->get_tagName(); + e->ignore(); + } } else e->ignore(); @@ -1043,6 +1089,8 @@ void container_qt::print( QPagedPaintDevice* paintDevice ) set_clip( clipRect, litehtml::border_radiuses() ); + clear_highlights(); + for ( auto page = 0; page < number_of_pages; page++ ) { mDocument->draw( reinterpret_cast( &painter ), 0, -page * ( scaled_printable_page_height ), &clipRect ); @@ -1053,3 +1101,206 @@ void container_qt::print( QPagedPaintDevice* paintDevice ) } del_clip(); } + +int container_qt::searchText( const QString& text ) +{ + search_text( mDocument, text.toStdString(), true ); + + // for ( auto& result : m_search_results ) + // { + // std::string ftxt; + // result.element->get_text( ftxt ); + // qDebug() << "found" << qPrintable( ftxt.c_str() ) << "at (" << result.element_pos.top() << result.element_pos.left() << ")"; + // } + + auto* result = get_current_result(); + + if ( result ) + { + horizontalScrollBar()->setValue( result->element_pos.left() ); + verticalScrollBar()->setValue( result->element_pos.top() ); + update(); + } + + return m_search_results.size(); +} + +// Rekursive Funktion zur Textsuche im DOM-Baum +void container_qt::search_text_in_element( litehtml::element::ptr el, + const std::string& search_term, + std::vector& results, + bool case_sensitive ) +{ + if ( !el ) + return; + + // Text des aktuellen Elements holen + std::string text; + el->get_text( text ); + if ( el->is_text() && !text.empty() ) + { + std::string element_text( text ); + std::string search_str = search_term; + + // Bei case-insensitive Suche beide Strings in Kleinbuchstaben umwandeln + if ( !case_sensitive ) + { + std::transform( element_text.begin(), element_text.end(), element_text.begin(), ::tolower ); + std::transform( search_str.begin(), search_str.end(), search_str.begin(), ::tolower ); + } + + // Alle Vorkommen im Text finden + size_t pos = 0; + while ( ( pos = element_text.find( search_str, pos ) ) != std::string::npos ) + { + TextSearchResult result; + result.matched_text = text.substr( pos, pos + search_term.length() ); // Original-Text (mit Groß-/Kleinschreibung) + result.element = el; + result.char_offset = static_cast( pos ); + result.element_pos = el->get_placement(); // Absolute Position im Dokument + + results.push_back( result ); + pos += search_term.length(); // Weiter nach dem Fund suchen + } + } + + // Rekursiv durch alle Kindelemente gehen + for ( auto it = el->children().begin(); it != el->children().end(); ++it ) + { + search_text_in_element( ( *it ), search_term, results, case_sensitive ); + } +} + +// Textsuche durchführen +int container_qt::search_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive ) +{ + m_search_results.clear(); + m_current_result_index = -1; + + if ( !doc || search_term.empty() ) + { + return 0; + } + + // Suche vom Root-Element starten + search_text_in_element( doc->root(), search_term, m_search_results, case_sensitive ); + + if ( !m_search_results.empty() ) + { + m_current_result_index = 0; + } + + return static_cast( m_search_results.size() ); +} + +// Zum nächsten Suchergebnis springen +bool container_qt::next_search_result() +{ + if ( m_search_results.empty() ) + return false; + + m_current_result_index++; + if ( m_current_result_index >= static_cast( m_search_results.size() ) ) + { + m_current_result_index = 0; // Wrap-around + } + return true; +} + +// Zum vorherigen Suchergebnis springen +bool container_qt::previous_search_result() +{ + if ( m_search_results.empty() ) + return false; + + m_current_result_index--; + if ( m_current_result_index < 0 ) + { + m_current_result_index = static_cast( m_search_results.size() ) - 1; // Wrap-around + } + return true; +} + +// Aktuelles Suchergebnis mit Position holen +const container_qt::TextSearchResult* container_qt::get_current_result() const +{ + if ( m_current_result_index >= 0 && m_current_result_index < static_cast( m_search_results.size() ) ) + { + return &m_search_results[m_current_result_index]; + } + return nullptr; +} + +void container_qt::draw_highlights( litehtml::uint_ptr hdc ) +{ + for ( auto it = m_search_results.begin(); it != m_search_results.end(); ++it ) + { + const auto result = ( *it ); + litehtml::position pos = result.element_pos; + highlight_text_at_position( hdc, pos, result.matched_text ); + } +} + +// // highlight current search result +// void container_qt::draw_current_highlight( litehtml::uint_ptr hdc ) +// { +// const TextSearchResult* result = get_current_result(); +// if ( !result ) +// return; + +// // position of found text +// litehtml::position pos = result->element_pos; + +// highlight_text_at_position( hdc, pos, result->matched_text ); +// } + +// draw highlighted text at position +void container_qt::highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const std::string& text ) +{ + + qDebug() << "Highlighting text '" << QString::fromStdString( text ) << "' at position (" << pos.x << ", " << pos.y << ") with size " << pos.width + << "x" << pos.height; + + QPainter* p( reinterpret_cast( hdc ) ); + p->save(); + applyClip( p ); + + p->setPen( Qt::NoPen ); + p->setBrush( mHighlightColor ); + + auto scroll_pos = -scrollBarPos(); + + p->drawRect( pos.x + scroll_pos.x(), pos.y + scroll_pos.y(), pos.width, pos.height ); + + p->restore(); +} + +void container_qt::scrollToNextSearchResult() +{ + if ( next_search_result() ) + { + const auto* result = get_current_result(); + + if ( result != nullptr ) + { + // force repaint to show highlighted text + horizontalScrollBar()->setValue( result->element_pos.left() ); + verticalScrollBar()->setValue( result->element_pos.top() ); + } + } +} + +void container_qt::scrollToPreviousSearchResult() +{ + if ( previous_search_result() ) + { + const auto* result = get_current_result(); + + if ( result != nullptr ) + { + // force repaint to show highlighted text + horizontalScrollBar()->setValue( result->element_pos.left() ); + verticalScrollBar()->setValue( result->element_pos.top() ); + } + } +} diff --git a/src/container_qt.h b/src/container_qt.h index adee6ab..8d9932a 100644 --- a/src/container_qt.h +++ b/src/container_qt.h @@ -31,6 +31,9 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co void setOpenExternalLinks( bool open ) { mOpenExternLinks = open; } const QString& caption() const { return mCaption; } void print( QPagedPaintDevice* paintDevice ); + int searchText( const QString& text ); + void scrollToNextSearchResult(); + void scrollToPreviousSearchResult(); protected: void paintEvent( QPaintEvent* ) override; @@ -73,7 +76,6 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co void get_media_features( litehtml::media_features& media ) const override; void get_language( litehtml::string& language, litehtml::string& culture ) const override; - void resizeEvent( QResizeEvent* event ) override; bool event( QEvent* event ) override; @@ -105,6 +107,29 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co MousePos convertMousePos( const QMouseEvent* event ); private: + // Struktur für gefundene Texttreffer mit Position + struct TextSearchResult + { + std::string matched_text; // Der gefundene Text + litehtml::position element_pos; // Position des Elements + int char_offset; // Zeichenoffset im Textknoten + litehtml::element::ptr element; // Zeiger auf das Element + }; + + void search_text_in_element( litehtml::element::ptr el, + const std::string& search_term, + std::vector& results, + bool case_sensitive = true ); + int search_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive = true ); + bool next_search_result(); + bool previous_search_result(); + const TextSearchResult* get_current_result() const; + const std::vector& get_all_results() const { return m_search_results; } + // void draw_current_highlight( litehtml::uint_ptr hdc ); + void highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const std::string& text ); + void draw_highlights( litehtml::uint_ptr hdc ); + void clear_highlights() { m_search_results.clear(); } + std::shared_ptr mDocument; QByteArray mDocumentSource; QUrl mBaseUrl; @@ -124,4 +149,7 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co QString mUserCSS; QStack mClipStack; litehtml::position mClip = {}; + std::vector m_search_results = {}; + int m_current_result_index = -1; + const QColor mHighlightColor = QColor( 255, 255, 0, 30 ); }; diff --git a/test/browser/testbrowser.cpp b/test/browser/testbrowser.cpp index 8fc1b79..e22a948 100644 --- a/test/browser/testbrowser.cpp +++ b/test/browser/testbrowser.cpp @@ -8,6 +8,8 @@ // #include #include #include +#include +#include TestBrowser::TestBrowser() { @@ -66,6 +68,38 @@ TestBrowser::TestBrowser() } } ); + mSearchText = new QLineEdit( this ); + mToolBar.addWidget( mSearchText ); + connect( mSearchText, &QLineEdit::editingFinished, this, + [this]() + { + if ( mSearchText ) + { + auto text = mSearchText->text(); + if ( !text.isEmpty() ) + { + searchText( mSearchText->text() ); + } + else + { + mPreviousSearchResult->setEnabled( false ); + mNextSearchResult->setEnabled( false ); + } + } + } ); + + // Vorheriges mit Standard-Icon (Pfeil nach oben) + mPreviousSearchResult = new QAction( style()->standardIcon( QStyle::SP_ArrowUp ), "Vorheriges", this ); + connect( mPreviousSearchResult, &QAction::triggered, this, [this]() { previousSearchResult(); } ); + mPreviousSearchResult->setEnabled( false ); + mToolBar.addAction( mPreviousSearchResult ); + + mNextSearchResult = new QAction( style()->standardIcon( QStyle::SP_ArrowDown ), "Nächstes", this ); + mNextSearchResult->setEnabled( false ); + connect( mNextSearchResult, &QAction::triggered, this, [this]() { nextSearchResult(); } ); + + mToolBar.addAction( mNextSearchResult ); + setMenuBar( &mMenu ); addToolBar( Qt::TopToolBarArea, &mToolBar ); mLastDirectory = QDir::current(); @@ -179,3 +213,22 @@ void TestBrowser::export2pdf() } #endif } +void TestBrowser::searchText( const QString& text ) +{ + if ( !text.isEmpty() ) + { + auto found = mBrowser->searchText( text ); + mPreviousSearchResult->setEnabled( found ); + mNextSearchResult->setEnabled( found ); + } +} + +void TestBrowser::previousSearchResult() +{ + mBrowser->previousSearchResult(); +} + +void TestBrowser::nextSearchResult() +{ + mBrowser->nextSearchResult(); +} diff --git a/test/browser/testbrowser.h b/test/browser/testbrowser.h index da525a7..ad2a01c 100644 --- a/test/browser/testbrowser.h +++ b/test/browser/testbrowser.h @@ -35,12 +35,18 @@ class TestBrowser : public QMainWindow void openHelp(); void loadHelp(); void export2pdf(); + void searchText( const QString& text ); + void nextSearchResult(); + void previousSearchResult(); private: QHelpBrowser* mBrowser; QMenuBar mMenu; QDir mLastDirectory; QLineEdit* mUrl = nullptr; + QLineEdit* mSearchText = nullptr; QToolBar mToolBar; QHelpEngine* mHelpEngine = nullptr; + QAction* mNextSearchResult = nullptr; + QAction* mPreviousSearchResult = nullptr; }; From 525d504fb8ae7efb1f88acfb3128fcccb4036c8b Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Wed, 22 Oct 2025 09:26:55 +0200 Subject: [PATCH 02/11] finished search --- CHANGELOG.md | 4 + include/qlitehtmlbrowser/QLiteHtmlBrowser.h | 6 +- src/QLiteHtmlBrowser.cpp | 13 +- src/QLiteHtmlBrowserImpl.cpp | 6 - src/QLiteHtmlBrowserImpl.h | 4 - src/container_qt.cpp | 250 ++++++++++++++------ src/container_qt.h | 34 ++- test/browser/testbrowser.cpp | 26 +- test/library/CMakeLists.txt | 1 + test/library/search/LongGermanText.html | 209 ++++++++++++++++ test/library/test_history.cpp | 8 - test/library/test_search.cpp | 79 +++++++ test/library/test_search.h | 33 +++ 13 files changed, 543 insertions(+), 130 deletions(-) create mode 100644 test/library/search/LongGermanText.html create mode 100644 test/library/test_search.cpp create mode 100644 test/library/test_search.h diff --git a/CHANGELOG.md b/CHANGELOG.md index d312eff..806eeb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## unreleased + +- [#20](https://github.com/procitec/qlitehtmlbrowser/issues/20): Feature Search Text + ## v2.1.0 - re-add support for Qt 5 diff --git a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h index 5a8f9e7..96f98a0 100644 --- a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h +++ b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h @@ -128,8 +128,6 @@ class QLITEHTMLBROWSER_EXPORT QLiteHtmlBrowser : public QWidget bool isBackwardAvailable() const; bool isForwardAvailable() const; void clearHistory(); - QString historyTitle( int ) const; - QUrl historyUrl( int ) const; int backwardHistoryCount() const; int forwardHistoryCount() const; @@ -141,8 +139,8 @@ class QLITEHTMLBROWSER_EXPORT QLiteHtmlBrowser : public QWidget /// search text in document int searchText( const QString& ); - void nextSearchResult(); - void previousSearchResult(); + void scrollToNextSearchResult(); + void scrollToPreviousSearchResult(); public Q_SLOTS: /// set URL to given url. The URL may be an url to local file, QtHelp, http etc. diff --git a/src/QLiteHtmlBrowser.cpp b/src/QLiteHtmlBrowser.cpp index cbd8eaf..548939b 100644 --- a/src/QLiteHtmlBrowser.cpp +++ b/src/QLiteHtmlBrowser.cpp @@ -130,15 +130,6 @@ void QLiteHtmlBrowser::clearHistory() mImpl->clearHistory(); } -QString QLiteHtmlBrowser::historyTitle( int idx ) const -{ - return mImpl->historyTitle( idx ); -} - -QUrl QLiteHtmlBrowser::historyUrl( int idx ) const -{ - return mImpl->historyUrl( idx ); -} int QLiteHtmlBrowser::backwardHistoryCount() const { @@ -178,12 +169,12 @@ int QLiteHtmlBrowser::searchText( const QString& text ) return mImpl->searchText( text ); } -void QLiteHtmlBrowser::nextSearchResult() +void QLiteHtmlBrowser::scrollToNextSearchResult() { mImpl->nextSearchResult(); } -void QLiteHtmlBrowser::previousSearchResult() +void QLiteHtmlBrowser::scrollToPreviousSearchResult() { mImpl->previousSearchResult(); } diff --git a/src/QLiteHtmlBrowserImpl.cpp b/src/QLiteHtmlBrowserImpl.cpp index b2c83e4..71d8bcb 100644 --- a/src/QLiteHtmlBrowserImpl.cpp +++ b/src/QLiteHtmlBrowserImpl.cpp @@ -407,12 +407,6 @@ void QLiteHtmlBrowserImpl::setOpenExternalLinks( bool open ) } } -QUrl QLiteHtmlBrowserImpl::historyUrl( int ) const -{ - // todo: implementation - qWarning() << "not implemented yet"; - return {}; -} void QLiteHtmlBrowserImpl::forward() { diff --git a/src/QLiteHtmlBrowserImpl.h b/src/QLiteHtmlBrowserImpl.h index d8bac2b..66a8926 100644 --- a/src/QLiteHtmlBrowserImpl.h +++ b/src/QLiteHtmlBrowserImpl.h @@ -58,10 +58,6 @@ class QLiteHtmlBrowserImpl : public QWidget void backward(); void reload(); - // todo from there is the title? - QString historyTitle( int ) const { return {}; } - - QUrl historyUrl( int ) const; int backwardHistoryCount() const { return ( 1 < mBWHistStack.count() ) ? mBWHistStack.count() - 1 : 0; } int forwardHistoryCount() const { return ( 0 < mFWHistStack.count() ) ? mFWHistStack.count() : 0; } diff --git a/src/container_qt.cpp b/src/container_qt.cpp index 798f887..ccdb9a3 100644 --- a/src/container_qt.cpp +++ b/src/container_qt.cpp @@ -1106,68 +1106,195 @@ int container_qt::searchText( const QString& text ) { search_text( mDocument, text.toStdString(), true ); - // for ( auto& result : m_search_results ) - // { - // std::string ftxt; - // result.element->get_text( ftxt ); - // qDebug() << "found" << qPrintable( ftxt.c_str() ) << "at (" << result.element_pos.top() << result.element_pos.left() << ")"; - // } - + viewport()->update(); auto* result = get_current_result(); - if ( result ) { - horizontalScrollBar()->setValue( result->element_pos.left() ); - verticalScrollBar()->setValue( result->element_pos.top() ); - update(); + scrollToSearchResult( result ); } return m_search_results.size(); } -// Rekursive Funktion zur Textsuche im DOM-Baum -void container_qt::search_text_in_element( litehtml::element::ptr el, - const std::string& search_term, - std::vector& results, - bool case_sensitive ) +// normalize Whitespace: multiple whitespaces to one +std::string container_qt::normalizeWhitespace( const std::string& text ) +{ + std::string result; + bool lastWasSpace = false; + + for ( char c : text ) + { + if ( std::isspace( static_cast( c ) ) ) + { + if ( !lastWasSpace && !result.empty() ) + { + result += ' '; + lastWasSpace = true; + } + } + else + { + result += c; + lastWasSpace = false; + } + } + + return result; +} + +void container_qt::collectTextFragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText ) { if ( !el ) return; - // Text des aktuellen Elements holen std::string text; el->get_text( text ); - if ( el->is_text() && !text.empty() ) + if ( !text.empty() && el->is_text() ) { - std::string element_text( text ); - std::string search_str = search_term; + TextFragment fragment; + fragment.text = text; + fragment.element = el; + fragment.pos = el->get_placement(); + fragment.start_offset = static_cast( fullText.length() ); + + std::string normalized = normalizeWhitespace( text ); + fullText += normalized; + + fragment.end_offset = static_cast( fullText.length() ); + fragments.push_back( fragment ); - // Bei case-insensitive Suche beide Strings in Kleinbuchstaben umwandeln - if ( !case_sensitive ) + // Leerzeichen zwischen Elementen + if ( !fullText.empty() && !std::isspace( fullText.back() ) ) { - std::transform( element_text.begin(), element_text.end(), element_text.begin(), ::tolower ); - std::transform( search_str.begin(), search_str.end(), search_str.begin(), ::tolower ); + fullText += ' '; } + } + + for ( auto it = el->children().begin(); it != el->children().end(); ++it ) + { + collectTextFragments( ( *it ), fragments, fullText ); + } +} + +// calculate bounding box for all elements found in search +litehtml::position container_qt::calculatePreciseBoundingBox( const std::vector& allFragments, + int searchStart, + int searchEnd, + std::vector& matchedFragments ) +{ + litehtml::position boundingBox = { 0, 0, 0, 0 }; + bool firstFragment = true; + + for ( const auto& fragment : allFragments ) + { + // check if this fragment is part of the search + int overlapStart = std::max( searchStart, fragment.start_offset ); + int overlapEnd = std::min( searchEnd, fragment.end_offset ); - // Alle Vorkommen im Text finden - size_t pos = 0; - while ( ( pos = element_text.find( search_str, pos ) ) != std::string::npos ) + if ( overlapStart < overlapEnd ) { - TextSearchResult result; - result.matched_text = text.substr( pos, pos + search_term.length() ); // Original-Text (mit Groß-/Kleinschreibung) - result.element = el; - result.char_offset = static_cast( pos ); - result.element_pos = el->get_placement(); // Absolute Position im Dokument - - results.push_back( result ); - pos += search_term.length(); // Weiter nach dem Fund suchen + // yes this fragment is part of the search + TextFragment matchedFragment = fragment; + + // offsets relative to start + int fragmentTextStart = overlapStart - fragment.start_offset; + int fragmentTextEnd = overlapEnd - fragment.start_offset; + + matchedFragment.start_offset = fragmentTextStart; + matchedFragment.end_offset = fragmentTextEnd; + + // calculate width of text + // Importnat: use text_width from document_container + std::string matchedText = fragment.text.substr( fragmentTextStart, fragmentTextEnd - fragmentTextStart ); + + // estimated position (kann mit text_width verfeinert werden) + litehtml::position fragmentPos = fragment.pos; + + // for more precise positioning text_width would be helpfull + // int textWidthBefore = container->text_width( + // fragment.text.substr(0, fragmentTextStart).c_str(), + // font + // ); + // fragmentPos.x += textWidthBefore; + // fragmentPos.width = container->text_width(matchedText.c_str(), font); + + // simplified approximation + if ( fragment.text.length() > 0 ) + { + float charWidth = static_cast( fragment.pos.width ) / static_cast( fragment.text.length() ); + fragmentPos.x += static_cast( charWidth * fragmentTextStart ); + fragmentPos.width = static_cast( charWidth * matchedText.length() ); + } + + matchedFragments.push_back( matchedFragment ); + + // extend Bounding Box + if ( firstFragment ) + { + boundingBox = fragmentPos; + firstFragment = false; + } + else + { + // Min/Max for containing Box + int minX = std::min( boundingBox.x, fragmentPos.x ); + int minY = std::min( boundingBox.y, fragmentPos.y ); + int maxX = std::max( boundingBox.x + boundingBox.width, fragmentPos.x + fragmentPos.width ); + int maxY = std::max( boundingBox.y + boundingBox.height, fragmentPos.y + fragmentPos.height ); + + boundingBox.x = minX; + boundingBox.y = minY; + boundingBox.width = maxX - minX; + boundingBox.height = maxY - minY; + } } } - // Rekursiv durch alle Kindelemente gehen - for ( auto it = el->children().begin(); it != el->children().end(); ++it ) + return boundingBox; +} + +// search document for Text including multiword phrases +void container_qt::searchTextInDocument( litehtml::document::ptr doc, + const std::string& search_term, + std::vector& results, + bool case_sensitive ) +{ + if ( !doc || search_term.empty() ) + return; + + // Sammle alle Text-Fragmente mit Positionen + std::vector fragments; + std::string fullText; + collectTextFragments( doc->root(), fragments, fullText ); + + // Normalisiere Text für Suche + std::string normalizedFullText = normalizeWhitespace( fullText ); + std::string normalizedSearchTerm = normalizeWhitespace( search_term ); + + std::string searchText = normalizedFullText; + std::string searchFor = normalizedSearchTerm; + + if ( !case_sensitive ) + { + std::transform( searchText.begin(), searchText.end(), searchText.begin(), ::tolower ); + std::transform( searchFor.begin(), searchFor.end(), searchFor.begin(), ::tolower ); + } + + // Alle Vorkommen finden + size_t pos = 0; + while ( ( pos = searchText.find( searchFor, pos ) ) != std::string::npos ) { - search_text_in_element( ( *it ), search_term, results, case_sensitive ); + TextSearchResult result; + result.matched_text = normalizedFullText.substr( pos, searchFor.length() ); + + int searchStart = static_cast( pos ); + int searchEnd = static_cast( pos + searchFor.length() ); + + // Berechne präzise Bounding Box + result.bounding_box = calculatePreciseBoundingBox( fragments, searchStart, searchEnd, result.fragments ); + + results.push_back( result ); + pos += searchFor.length(); } } @@ -1183,7 +1310,8 @@ int container_qt::search_text( litehtml::document::ptr doc, const std::string& s } // Suche vom Root-Element starten - search_text_in_element( doc->root(), search_term, m_search_results, case_sensitive ); + // search_text_in_element( doc->root(), search_term, m_search_results, case_sensitive ); + searchTextInDocument( mDocument, search_term, m_search_results, case_sensitive ); if ( !m_search_results.empty() ) { @@ -1236,24 +1364,14 @@ void container_qt::draw_highlights( litehtml::uint_ptr hdc ) for ( auto it = m_search_results.begin(); it != m_search_results.end(); ++it ) { const auto result = ( *it ); - litehtml::position pos = result.element_pos; - highlight_text_at_position( hdc, pos, result.matched_text ); + for ( auto it = result.fragments.begin(); it != result.fragments.end(); ++it ) + { + litehtml::position pos = ( *it ).pos; + highlight_text_at_position( hdc, pos, result.matched_text ); + } } } -// // highlight current search result -// void container_qt::draw_current_highlight( litehtml::uint_ptr hdc ) -// { -// const TextSearchResult* result = get_current_result(); -// if ( !result ) -// return; - -// // position of found text -// litehtml::position pos = result->element_pos; - -// highlight_text_at_position( hdc, pos, result->matched_text ); -// } - // draw highlighted text at position void container_qt::highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const std::string& text ) { @@ -1280,13 +1398,7 @@ void container_qt::scrollToNextSearchResult() if ( next_search_result() ) { const auto* result = get_current_result(); - - if ( result != nullptr ) - { - // force repaint to show highlighted text - horizontalScrollBar()->setValue( result->element_pos.left() ); - verticalScrollBar()->setValue( result->element_pos.top() ); - } + scrollToSearchResult( result ); } } @@ -1295,12 +1407,16 @@ void container_qt::scrollToPreviousSearchResult() if ( previous_search_result() ) { const auto* result = get_current_result(); + scrollToSearchResult( result ); + } +} - if ( result != nullptr ) - { - // force repaint to show highlighted text - horizontalScrollBar()->setValue( result->element_pos.left() ); - verticalScrollBar()->setValue( result->element_pos.top() ); - } +void container_qt::scrollToSearchResult( const TextSearchResult* result ) +{ + if ( !result ) + { + return; } + horizontalScrollBar()->setValue( result->bounding_box.left() ); + verticalScrollBar()->setValue( result->bounding_box.top() ); } diff --git a/src/container_qt.h b/src/container_qt.h index 8d9932a..d9aff44 100644 --- a/src/container_qt.h +++ b/src/container_qt.h @@ -107,28 +107,40 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co MousePos convertMousePos( const QMouseEvent* event ); private: - // Struktur für gefundene Texttreffer mit Position + struct TextFragment + { + std::string text; + litehtml::element::ptr element; + litehtml::position pos; + int start_offset; // Offset in element text + int end_offset; // end Offset in element text + }; + struct TextSearchResult { - std::string matched_text; // Der gefundene Text - litehtml::position element_pos; // Position des Elements - int char_offset; // Zeichenoffset im Textknoten - litehtml::element::ptr element; // Zeiger auf das Element + std::string matched_text; + std::vector fragments; // may contains multiple elements + litehtml::position bounding_box; // bounding box over all elements }; - void search_text_in_element( litehtml::element::ptr el, - const std::string& search_term, - std::vector& results, - bool case_sensitive = true ); int search_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive = true ); bool next_search_result(); bool previous_search_result(); const TextSearchResult* get_current_result() const; - const std::vector& get_all_results() const { return m_search_results; } - // void draw_current_highlight( litehtml::uint_ptr hdc ); void highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const std::string& text ); void draw_highlights( litehtml::uint_ptr hdc ); void clear_highlights() { m_search_results.clear(); } + std::string normalizeWhitespace( const std::string& text ); + void searchTextInDocument( litehtml::document::ptr doc, + const std::string& search_term, + std::vector& results, + bool case_sensitive = true ); + void collectTextFragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText ); + litehtml::position calculatePreciseBoundingBox( const std::vector& allFragments, + int searchStart, + int searchEnd, + std::vector& matchedFragments ); + void scrollToSearchResult( const TextSearchResult* ); std::shared_ptr mDocument; QByteArray mDocumentSource; diff --git a/test/browser/testbrowser.cpp b/test/browser/testbrowser.cpp index e22a948..c576793 100644 --- a/test/browser/testbrowser.cpp +++ b/test/browser/testbrowser.cpp @@ -70,21 +70,12 @@ TestBrowser::TestBrowser() mSearchText = new QLineEdit( this ); mToolBar.addWidget( mSearchText ); - connect( mSearchText, &QLineEdit::editingFinished, this, + connect( mSearchText, &QLineEdit::returnPressed, this, [this]() { - if ( mSearchText ) + if ( mSearchText != nullptr ) { - auto text = mSearchText->text(); - if ( !text.isEmpty() ) - { - searchText( mSearchText->text() ); - } - else - { - mPreviousSearchResult->setEnabled( false ); - mNextSearchResult->setEnabled( false ); - } + searchText( mSearchText->text() ); } } ); @@ -215,20 +206,17 @@ void TestBrowser::export2pdf() } void TestBrowser::searchText( const QString& text ) { - if ( !text.isEmpty() ) - { auto found = mBrowser->searchText( text ); - mPreviousSearchResult->setEnabled( found ); - mNextSearchResult->setEnabled( found ); - } + mPreviousSearchResult->setEnabled( found > 0 ); + mNextSearchResult->setEnabled( found > 0 ); } void TestBrowser::previousSearchResult() { - mBrowser->previousSearchResult(); + mBrowser->scrollToPreviousSearchResult(); } void TestBrowser::nextSearchResult() { - mBrowser->nextSearchResult(); + mBrowser->scrollToNextSearchResult(); } diff --git a/test/library/CMakeLists.txt b/test/library/CMakeLists.txt index 1a1cb5c..2572496 100644 --- a/test/library/CMakeLists.txt +++ b/test/library/CMakeLists.txt @@ -12,6 +12,7 @@ set( test_names test_html_content test_css_content test_history + test_search ) set( runpath "$;$") diff --git a/test/library/search/LongGermanText.html b/test/library/search/LongGermanText.html new file mode 100644 index 0000000..cd149b8 --- /dev/null +++ b/test/library/search/LongGermanText.html @@ -0,0 +1,209 @@ + + + + +200 Paragraphen deutscher Fließtext + + +

Absatz 1: Ein kurzer deutscher Beispielsatz zur Illustration von Fließtext ohne Stilangaben. Eine zweite Aussage ergänzt den Gedanken und hält die Sprache natürlich. Gelegentlich folgt ein weiterer Satz, der eine alltägliche Beobachtung beschreibt. Manchmal endet der Absatz mit einer ruhigen Feststellung.

+

Absatz 2: Dieser Abschnitt zeigt einfachen Text ohne besondere Formatierung. Die Sätze sind bewusst klar und unaufgeregt formuliert. So bleibt der Inhalt leicht lesbar und gut kopierbar. Am Ende steht eine sachliche Abrundung.

+

Absatz 3: Hier wird ein neutraler Ton beibehalten, um universell einsetzbaren Inhalt zu liefern. Die Wortwahl ist unprätentiös und verständlich. Dadurch eignet sich der Text gut für Demonstrationszwecke. Ein ruhiger Abschluss rundet den Absatz ab.

+

Absatz 4: Der Text beschreibt keine speziellen Ereignisse und bleibt allgemein. Das erleichtert die Wiederverwendung in unterschiedlichen Kontexten. Gleichzeitig wirkt die Sprache flüssig und natürlich. Ein letzter Satz beendet den Gedanken.

+

Absatz 5: Diese Zeilen vermitteln schlichte Aussagen ohne Ablenkungen. Die Struktur folgt einem gleichmäßigen Rhythmus. Damit entsteht ein unaufdringlicher Lesefluss. Am Schluss steht eine kurze Zusammenfassung.

+

Absatz 6: Der Fokus liegt auf klarer Verständlichkeit und Einfachheit. Jeder Satz trägt eine kleine, abgeschlossene Beobachtung. Das hilft beim problemlosen Kopieren und Einfügen. Ein sachlicher Ausklang beschließt den Abschnitt.

+

Absatz 7: Der Aufbau bleibt über alle Absätze hinweg konsistent. So entsteht ein verlässliches Muster für Tests und Beispiele. Die Sprache bleibt bewusst nüchtern. Ein knapper Schlusssatz hält die Form.

+

Absatz 8: Dieser Absatz dient als weiterer Baustein für umfangreichen Text. Die Inhalte sind absichtlich generisch gehalten. Dadurch bleibt der Einsatz flexibel und unkompliziert. Ein kurzer Abschluss beendet den Gedanken.

+

Absatz 9: Der Text verzichtet vollständig auf Stildefinitionen. Damit eignet er sich für reine HTML-Demonstrationen. Die Sätze bleiben in einer sachlichen Tonlage. Ein letzter Satz schließt den Abschnitt.

+

Absatz 10: Hier wird die Lesbarkeit priorisiert und unnötige Details vermieden. Die Aussagen sind klar trennbar und kompakt. So bleibt der Inhalt gut strukturiert. Ein ruhiger Abschluss rundet die Passage ab.

+

Absatz 11: Die Formulierungen wirken bewusst neutral und zeitlos. Dadurch bleibt der Text langfristig verwendbar. Der Aufbau unterstützt ein gleichmäßiges Erfassen. Ein kurzer Schlusspunkt vollendet den Abschnitt.

+

Absatz 12: Jeder Absatz enthält mehrere Sätze für einen natürlichen Fluss. Die Themen bleiben allgemein und unverfänglich. Das macht den Text vielseitig nutzbar. Ein sachlicher Ausklang beschließt das Stück.

+

Absatz 13: Die Wortwahl ist einfach und direkt gehalten. So entsteht eine klare und ruhige Sprache. Der Zweck liegt im ungestörten Kopieren. Ein letzter Satz fasst die Idee zusammen.

+

Absatz 14: Diese Zeilen zeigen unverzierten Inhalt in regulären Sätzen. Die Struktur ist leicht zu erkennen. Damit eignet sich der Text für Tests und Layouts. Ein kurzer Abschluss beendet das Beispiel.

+

Absatz 15: Der Abschnitt verzichtet auf Metaphern und Fachbegriffe. Dadurch bleibt alles unmissverständlich formuliert. Die Sätze sind bewusst übersichtlich gestaltet. Ein ruhiger Schluss bewahrt die Form.

+

Absatz 16: Es wird ein konstantes Sprachniveau gehalten. Das erleichtert das Querlesen und Vergleichen. Die Länge der Sätze bleibt moderat. Ein abschließender Satz sorgt für Vollständigkeit.

+

Absatz 17: Der Inhalt ist für verschiedenste Zwecke einsetzbar. Er eignet sich für Prototypen oder Demonstrationen. Zugleich bleibt die Sprache angenehm schlicht. Ein kurzer Ausklang beendet den Absatz.

+

Absatz 18: Die Gestaltung ist auf Klarheit ausgerichtet. Alle Sätze sind eigenständig verständlich. So entstehen klare Einheiten im Fließtext. Ein letzter Satz schließt ab.

+

Absatz 19: Dieser Text unterstützt das Testen von Strukturen. Er ist lang genug für realistische Beispiele. Gleichzeitig bleibt er leicht. Ein kurzer Abschluss hält die Ordnung.

+

Absatz 20: Die Sätze folgen einem vorhersehbaren Muster. Dadurch sind Auswertungen gut möglich. Der Inhalt lenkt nicht vom Zweck ab. Ein ruhiger Schlusssatz beendet die Passage.

+

Absatz 21: Der Ton bleibt neutral und gleichmäßig. Das macht den Text kompatibel mit vielen Szenarien. Die Lesbarkeit steht im Vordergrund. Ein kurzer Schluss rundet ab.

+

Absatz 22: Die Aussage beschränkt sich auf allgemeine Beobachtungen. Fachliche Tiefe wird bewusst vermieden. Damit bleiben die Zeilen vielseitig. Ein knapper Abschluss beendet den Abschnitt.

+

Absatz 23: Die Gliederung ist klar und stabil. So lässt sich der Text gut segmentieren. Der Stil bleibt unaufdringlich. Ein letztes Wort beendet den Gedanken.

+

Absatz 24: Diese Passage reiht sich in das Muster ein. Sie liefert mehrere, klar getrennte Sätze. Das unterstützt die Wiederverwendung. Ein kurzer Schluss beschließt den Absatz.

+

Absatz 25: Der Text erfüllt die Funktion eines Platzhalters. Er ist verständlich und zurückhaltend. Dadurch entstehen keine Missverständnisse. Ein ruhiger Abschluss sorgt für Balance.

+

Absatz 26: Die Sätze sind unabhängig voneinander sinnvoll. Gleichzeitig ergeben sie eine runde Einheit. Das erleichtert das Kopieren in andere Kontexte. Ein einfacher Schlusssatz vollendet die Zeilen.

+

Absatz 27: Der Abschnitt bleibt frei von Fachtermini. Die Aussage ist leicht zu greifen. Dadurch wirkt der Text universell. Ein kurzer Abschluss hält die Struktur.

+

Absatz 28: Die Form ist funktional und übersichtlich. Jedes Statement steht für sich. So bleibt der Überblick erhalten. Ein letzter Satz beendet die Passage.

+

Absatz 29: Der Inhalt ist bewusst generisch. Er passt zu vielen Anwendungsfällen. Das sorgt für Flexibilität. Ein Schluss bringt Ruhe in den Absatz.

+

Absatz 30: Die Sprache ist geradlinig und ruhig. Die Sätze tragen kleine, abgeschlossene Gedanken. Damit en;tsteht ein gleichmäßiger Rhythmus. Ein sachlicher Abschluss folgt.

+

Absatz 31: Diese Zeilen stützen Standardtests und Beispiele. Der Stil bleibt gleichförmig und verständlich. So wird die Anwendung erleichtert. Ein kurzer Schlusssatz schließt ab.

+

Absatz 32: Die Passage dient der Veranschaulichung neutraler Inhalte. Der Text lenkt nicht ab. Er bleibt freundlich sachlich. Ein knapper Abschluss beendet den Abschnitt.

+

Absatz 33: Der Fokus liegt auf Lesbarkeit und Einfachheit. Jedes Element ist leicht zu erfassen. Dadurch entstehen klare Strukturen. Ein ruhiger Schluss rundet ab.

+

Absatz 34: Die Sätze sind bewusst moderat lang. Sie tragen jeweils einen klaren Gedanken. So entsteht eine angenehme Abfolge. Ein letzter Satz beschließt den Absatz.

+

Absatz 35: Der Text bleibt frei von Bewertungen. Er stellt lediglich neutrale Aussagen bereit. Das erhöht die Übertragbarkeit. Ein sachlicher Abschluss folgt.

+

Absatz 36: Dieser Abschnitt fügt sich in das Gesamtbild ein. Er hält am konsistenten Ton fest. Die Lesbarkeit bleibt hoch. Ein kurzer Schluss beendet ihn.

+

Absatz 37: Die Inhalte sind allgemeingültig formuliert. Sie funktionieren in diversen Zusammenhängen. Die Sprache bleibt zurückhaltend. Ein letzter Satz bildet den Abschluss.

+

Absatz 38: Die Absicht ist reine Demonstration. Es gibt keine speziellen Bezüge. Dadurch bleibt der Text zeitlos. Ein sachlicher Schlusssatz folgt.

+

Absatz 39: Die Struktur unterstützt das schnelle Erfassen. Die Sätze sind klar getrennt. So bleibt der Fluss erhalten. Ein Schluss beendet den Gedanken.

+

Absatz 40: Der Abschnitt ist stromlinienförmig verfasst. Er vermeidet unnötige Ausschmückungen. Das fördert die Klarheit. Ein kurzer Abschluss rundet ab.

+

Absatz 41: Die Formulierungen sind bewusst schlicht gehalten. Jede Aussage steht für sich. Der Text bleibt gut übertragbar. Ein letzter Satz beschließt den Absatz.

+

Absatz 42: Die Zeilen fügen sich reibungslos ein. Es gibt keine stilistischen Ausreißer. So entsteht ein ruhiges Gesamtbild. Ein sachlicher Schluss folgt.

+

Absatz 43: Der Ton ist nüchtern und beständig. Die Sätze sind klar strukturiert. Das erleichtert die Nutzung. Ein kurzer Abschluss beendet die Passage.

+

Absatz 44: Die Inhalte bleiben auf einer allgemeinen Ebene. Gleichzeitig sind sie konkret genug. Dadurch entsteht Verlässlichkeit. Ein Schlusssatz fasst zusammen.

+

Absatz 45: Die Passage ist frei von Ablenkungen. Sie erfüllt eine klare Funktion. Der Text bleibt minimalistisch. Ein ruhiger Abschluss folgt.

+

Absatz 46: Der Abschnitt trägt zum Gesamtumfang bei. Er wiederholt das Muster bewusst. So bleibt die Konsistenz erhalten. Ein kurzer Schluss beendet den Teil.

+

Absatz 47: Die Sätze enden jeweils in einem klaren Punkt. Dadurch sind sie gut trennbar. Das dient der Übersichtlichkeit. Ein letzter Satz schließt ab.

+

Absatz 48: Die Sprache wirkt neutral und geordnet. Es gibt keine überraschenden Wendungen. Das unterstützt verlässliche Tests. Ein sachlicher Abschluss folgt.

+

Absatz 49: Der Text verzichtet auf rhetorische Mittel. Die Aussage ist direkt und ruhig. Das hält den Fokus klar. Ein kurzer Schluss beendet den Absatz.

+

Absatz 50: Die Struktur ist simpel und stabil. Jeder Satz trägt einen eigenen Sinn. So bleibt alles nachvollziehbar. Ein letzter Satz fasst knapp zusammen.

+

Absatz 51: Der Ton bleibt durchgehend sachlich. Das erleichtert die Wiederverwendung. Der Aufbau ist leicht zu erkennen. Ein ruhiger Abschluss folgt.

+

Absatz 52: Die Sätze sind grammatikalisch unkompliziert. Dadurch entsteht ein flüssiger Lesefluss. Der Inhalt bleibt allgemein. Ein kurzer Schlusssatz beendet die Zeilen.

+

Absatz 53: Der Abschnitt passt in jedes neutrale Layout. Er ist bewusst zurückhaltend formuliert. So fügt er sich problemlos ein. Ein sachlicher Abschluss folgt.

+

Absatz 54: Die Aussagen sind unabhängig von Fachgebieten. Das macht sie flexibel einsetzbar. Der Stil bleibt klar. Ein abschließender Satz beschließt den Absatz.

+

Absatz 55: Der Text erfüllt eine klare Demonstrationsaufgabe. Er bleibt frei von Details. Dadurch ist er vielseitig. Ein kurzer Schluss beendet den Abschnitt.

+

Absatz 56: Die Zeilen sind moderat lang und ruhig. Jede Aussage wirkt in sich geschlossen. Das verbessert die Struktur. Ein letzter Satz rundet ab.

+

Absatz 57: Die Form ist minimalistisch und zweckmäßig. Der Inhalt bleibt neutral. So entsteht ein universeller Baustein. Ein kurzer Abschluss schließt den Absatz.

+

Absatz 58: Die Sprache ist nüchtern und übersichtlich. Es gibt keine Ablenkung vom Zweck. Das erleichtert das Kopieren. Ein sachlicher Schluss beendet die Passage.

+

Absatz 59: Der Abschnitt setzt das gewählte Muster fort. Er bleibt in Ton und Struktur konstant. Dadurch entsteht Verlässlichkeit. Ein kurzer Abschluss folgt.

+

Absatz 60: Die Sätze liefern kleine, abgeschlossene Ideen. Sie sind sofort verständlich. Das unterstützt die Lesbarkeit. Ein ruhiger Schlusssatz beschließt den Teil.

+

Absatz 61: Diese Zeilen sind für Tests gedacht. Der Inhalt ist absichtlich generisch. So entsteht Unabhängigkeit vom Kontext. Ein kurzer Schluss beendet den Absatz.

+

Absatz 62: Die Wortwahl ist unaufgeregt und glatt. Das hält die Form sachlich. Der Lesefluss bleibt gleichmäßig. Ein letzter Satz rundet ab.

+

Absatz 63: Die Struktur bleibt durchgehend konsistent. So sind Auswertungen gut möglich. Der Text lenkt nicht ab. Ein kurzer Abschluss folgt.

+

Absatz 64: Der Ton ist freundlich neutral. Die Sätze sind gut getrennt. Das macht alles übersichtlich. Ein letzter Satz beendet die Passage.

+

Absatz 65: Der Abschnitt trägt zur Länge bei. Er wiederholt das grundlegende Schema. So bleibt der Gesamteindruck geschlossen. Ein sachlicher Schluss folgt.

+

Absatz 66: Die Inhalte sind leicht anzupassen. Sie stehen auf allgemeiner Ebene. Das ermöglicht vielfältige Nutzung. Ein kurzer Abschluss beendet den Teil.

+

Absatz 67: Die Sprache bleibt bodenständig und schlicht. Jede Aussage ist klar erkennbar. Dadurch entsteht Stabilität. Ein letzter Satz schließt ab.

+

Absatz 68: Der Text wirkt geordnet und ruhig. Es gibt keine unnötigen Ausschmückungen. Das erhöht die Klarheit. Ein ruhiger Abschluss folgt.

+

Absatz 69: Der Abschnitt ist ohne formale Besonderheiten. Die Sätze stehen im Vordergrund. So entsteht ein pures Beispiel. Ein kurzer Schluss beendet die Passage.

+

Absatz 70: Die Formulierung ist bewusst generisch. Sie passt in viele Umgebungen. Das macht den Text vielseitig. Ein sachlicher Abschluss folgt.

+

Absatz 71: Die Aussagen sind verständlich und knapp. Dadurch bleibt der Fokus erhalten. Der Lesefluss ist gleichmäßig. Ein letzter Satz beschließt den Absatz.

+

Absatz 72: Der Ton bleibt konstant sachlich. Das sorgt für Berechenbarkeit. Die Struktur ist stabil. Ein kurzer Schlusssatz beendet den Abschnitt.

+

Absatz 73: Die Zeilen ordnen sich dem Zweck unter. Sie bilden neutrale Beispiele. So bleibt alles flexibel. Ein ruhiger Abschluss folgt.

+

Absatz 74: Die Sätze sind funktional und klar. Sie tragen kleine Informationen. Dadurch ist der Text gut einsetzbar. Ein kurzer Schluss beendet den Teil.

+

Absatz 75: Der Abschnitt fügt Volumen hinzu. Er stört die Einheit nicht. Das Muster bleibt erkennbar. Ein letzter Satz schließt ab.

+

Absatz 76: Die Inhalte sind bewusst unauffällig. Sie erfüllen ihren Zweck. So bleibt die Aufmerksamkeit beim Aufbau. Ein sachlicher Abschluss folgt.

+

Absatz 77: Die Worte sind schlicht und direkt. Dadurch lässt sich alles leicht lesen. Der Text bleibt angenehm. Ein kurzer Schluss beendet die Passage.

+

Absatz 78: Die Form ist beständig und klar. Sie vermeidet Überraschungen. Das unterstützt Verlässlichkeit. Ein ruhiger Abschluss folgt.

+

Absatz 79: Der Text ist universell nutzbar. Er hat keine speziellen Bezüge. So bleibt er zeitlos. Ein Schlusssatz fasst zusammen.

+

Absatz 80: Die Sätze sind übersichtlich arrangiert. Dadurch entstehen klare Einheiten. Das erleichtert das Kopieren. Ein letzter Satz beschließt den Absatz.

+

Absatz 81: Der Abschnitt knüpft an die bisherigen an. Er hält die Linie ein. Damit bleibt alles stimmig. Ein kurzer Abschluss folgt.

+

Absatz 82: Die Aussagen sind sachlich und ruhig. Es gibt keine Wertung. Das bewahrt die Neutralität. Ein ruhiger Schluss beendet den Teil.

+

Absatz 83: Die Wortwahl ist sorgfältig einfach. Dadurch ist die Sprache klar. Das fördert die Lesbarkeit. Ein sachlicher Abschluss folgt.

+

Absatz 84: Die Struktur liefert Verlässlichkeit. Jedes Element erfüllt seine Rolle. So bleibt alles geordnet. Ein letzter Satz schließt ab.

+

Absatz 85: Die Zeilen sind für vielfältige Tests geeignet. Sie bleiben inhaltlich offen. Das erhöht den Nutzen. Ein kurzer Schluss beendet den Abschnitt.

+

Absatz 86: Die Sätze lassen sich isoliert betrachten. Gleichzeitig ergeben sie eine Einheit. So entsteht Balance. Ein ruhiger Abschluss folgt.

+

Absatz 87: Der Text ist in vielen Kontexten passend. Er ist neutral formuliert. Dadurch wirkt er flexibel. Ein letzter Satz beschließt den Absatz.

+

Absatz 88: Die Sprache ist schlicht und ordentlich. Es gibt keinen überflüssigen Schmuck. Das hält den Fokus klar. Ein kurzer Schluss beendet die Passage.

+

Absatz 89: Die Inhalte sind bewusst generisch gehalten. Der Ton bleibt gleichförmig. So entsteht Kontinuität. Ein sachlicher Abschluss folgt.

+

Absatz 90: Die Sätze sind klar voneinander getrennt. Das erleichtert das Zitieren. Der Fluss bleibt ruhig. Ein letzter Satz rundet ab.

+

Absatz 91: Diese Passage stärkt das konsistente Gesamtbild. Sie verzichtet auf Besonderheiten. Dadurch bleibt die Linie erhalten. Ein kurzer Schluss beendet den Teil.

+

Absatz 92: Der Text ist funktional ausgerichtet. Er vermeidet thematische Tiefe. So bleibt er vielseitig. Ein ruhiger Abschluss folgt.

+

Absatz 93: Die Sprache wirkt neutral und gelassen. Die Sätze sind leicht verständlich. Das erhöht die Zugänglichkeit. Ein abschließender Satz folgt.

+

Absatz 94: Der Abschnitt fügt sich reibungslos ein. Die Struktur bleibt vertraut. So entsteht ein verlässliches Muster. Ein kurzer Schluss beendet die Passage.

+

Absatz 95: Die Aussagen sind knapp und präzise. Dadurch bleibt die Orientierung einfach. Der Text ist gut einsetzbar. Ein letzter Satz beschließt den Absatz.

+

Absatz 96: Die Formulierung ist bewusst zurückhaltend. Sie dient dem Zweck der Demonstration. Das macht alles planbar. Ein ruhiger Abschluss folgt.

+

Absatz 97: Der Ton bleibt unverändert sachlich. Es gibt keine Stilwechsel. So bleibt der Eindruck konsistent. Ein kurzer Schluss beendet den Teil.

+

Absatz 98: Die Sätze tragen einzelne Gedanken. Zusammen formen sie eine Einheit. Das erleichtert das Verständnis. Ein abschließender Satz folgt.

+

Absatz 99: Der Text vermeidet komplexe Konstruktionen. Dadurch bleibt er einfach lesbar. Die Struktur wirkt klar. Ein Schlusssatz rundet ab.

+

Absatz 100: Die Passage ergänzt den Bestand verlässlich. Sie hält überall das Muster. So bleibt die Qualität gleich. Ein kurzer Abschluss beschließt den Abschnitt.

+

Absatz 101: Die Zeilen sind unaufgeregt und stabil. Jede Aussage steht ruhig da. Das erhöht die Lesbarkeit. Ein letzter Satz beendet den Absatz.

+

Absatz 102: Der Inhalt ist leicht übertragbar. Er hat keine Abhängigkeiten. So bleibt er flexibel. Ein kurzer Schluss folgt.

+

Absatz 103: Die Sprache verzichtet auf Fachsprache. Dadurch ist sie allgemein verständlich. Das hilft in vielen Szenarien. Ein ruhiger Abschluss beendet die Passage.

+

Absatz 104: Die Struktur stärkt die Übersicht. Alles bleibt leicht zu erfassen. So entsteht Ordnung. Ein Schlusssatz komplettiert den Teil.

+

Absatz 105: Die Sätze sind klar und direkt. Sie wirken in sich abgeschlossen. Das macht den Text robust. Ein kurzer Abschluss folgt.

+

Absatz 106: Die Form ist für Demonstrationen gedacht. Sie bleibt bewusst einfach. Dadurch sinkt die Fehleranfälligkeit. Ein letzter Satz rundet ab.

+

Absatz 107: Der Abschnitt setzt auf Neutralität. Er verzichtet auf Bewertungen. So bleibt er universell. Ein kurzer Schluss beendet die Zeilen.

+

Absatz 108: Die Aussagen tragen einfache Ideen. Sie sind schnell erfassbar. Das unterstützt den Zweck. Ein sachlicher Abschluss folgt.

+

Absatz 109: Die Passage fügt sich organisch ein. Ton und Form bleiben gleich. Das schützt die Konsistenz. Ein letzter Satz beschließt den Absatz.

+

Absatz 110: Die Sprache ist ruhig und geordnet. Es gibt keine komplexen Wendungen. Das steigert die Klarheit. Ein kurzer Abschluss beendet die Passage.

+

Absatz 111: Der Text bleibt ohne Stildefinitionen. Dadurch ist er unverfälscht. Das eignet sich für Rohformen. Ein Schlusssatz folgt.

+

Absatz 112: Die Struktur bleibt verlässlich stabil. So sind Prüfungen gut möglich. Der Lesefluss ist regelmäßig. Ein ruhiger Abschluss rundet ab.

+

Absatz 113: Die Sätze stehen für sich. Zusammen bilden sie einen Abschnitt. Das schafft Übersicht. Ein kurzer Schluss beendet den Teil.

+

Absatz 114: Die Inhalte bleiben allgemein und offen. Dadurch sind sie vielseitig verwendbar. Der Ton ist neutral. Ein letzter Satz schließt ab.

+

Absatz 115: Der Abschnitt enthält nur wesentliche Aussagen. Nichts lenkt vom Zweck ab. So bleibt alles fokussiert. Ein sachlicher Abschluss folgt.

+

Absatz 116: Die Zeilen sind bewusst geradlinig. Das erleichtert das Kopieren. Der Eindruck bleibt ruhig. Ein kurzer Schluss beendet die Passage.

+

Absatz 117: Die Sprache ist schlicht und ordentlich. Jeder Satz trägt einen Punkt. Das hält den Rhythmus. Ein letzter Satz rundet ab.

+

Absatz 118: Die Form ist gleichmäßig gewählt. Sie vermeidet Brüche. So entsteht Verlässlichkeit. Ein sachlicher Abschluss folgt.

+

Absatz 119: Der Text bleibt universell interpretierbar. Es gibt keine speziellen Bezüge. Das macht ihn flexibel. Ein kurzer Schluss beendet den Teil.

+

Absatz 120: Die Sätze sind griffig und klar. Dadurch sind sie direkt nutzbar. Der Lesefluss bleibt eben. Ein ruhiger Abschluss folgt.

+

Absatz 121: Die Zeilen sind bewusst nüchtern. Das hält die Struktur transparent. So ergeben sich klare Blöcke. Ein letzter Satz beendet den Absatz.

+

Absatz 122: Die Aussagen sind allgemein gehalten. Dadurch sind sie kompatibel. Der Ton ist sachlich. Ein kurzer Schluss beschließt die Passage.

+

Absatz 123: Der Abschnitt stärkt das Muster. Er folgt denselben Prinzipien. So bleibt die Einheit gewahrt. Ein abschließender Satz folgt.

+

Absatz 124: Die Inhalte sind leicht zu lesen. Sie wirken ruhig und klar. Das unterstützt den Zweck. Ein kurzer Abschluss beendet den Teil.

+

Absatz 125: Der Text verzichtet auf Beispiele. Er bleibt bewusst abstrakt. Dadurch ist er überall einsetzbar. Ein sachlicher Schluss folgt.

+

Absatz 126: Die Sätze sind sauber getrennt. So wird die Struktur deutlich. Das schafft Übersicht. Ein letzter Satz schließt ab.

+

Absatz 127: Die Formulierungen sind unaufgeregt. Dadurch entsteht Stabilität. Der Text liest sich angenehm. Ein kurzer Schluss beendet die Passage.

+

Absatz 128: Die Passage ist klar gegliedert. Sie passt in viele Muster. So bleibt sie nützlich. Ein ruhiger Abschluss folgt.

+

Absatz 129: Die Inhalte sind zeitlos gedacht. Es fehlen konkrete Anker. Dadurch bleibt der Text flexibel. Ein sachlicher Abschluss beschließt den Absatz.

+

Absatz 130: Die Sprache bleibt zurückgenommen. Sie dient dem Zweck verlässlich. Das stärkt die Nutzbarkeit. Ein kurzer Schluss beendet den Teil.

+

Absatz 131: Der Abschnitt setzt auf Verständlichkeit. Die Aussagen sind eindeutig. So entsteht Klarheit. Ein letzter Satz rundet ab.

+

Absatz 132: Die Sätze sind gleichmäßig lang. Dadurch bleibt der Rhythmus stabil. Der Text ist angenehm zu lesen. Ein kurzer Abschluss folgt.

+

Absatz 133: Die Form wirkt vertraut und nüchtern. Das unterstützt die Orientierung. So bleibt alles greifbar. Ein abschließender Satz schließt ab.

+

Absatz 134: Die Zeilen sind sparsam formuliert. Nichts ist überladen. Dadurch bleibt die Prägnanz erhalten. Ein ruhiger Schluss beendet die Passage.

+

Absatz 135: Der Abschnitt ist konsequent neutral. Er verzichtet auf Tonwechsel. Das schafft Einheit. Ein kurzer Abschluss folgt.

+

Absatz 136: Die Sprache wirkt kontrolliert und klar. Jede Aussage ist verständlich. Das hilft beim Kopieren. Ein letzter Satz rundet ab.

+

Absatz 137: Der Text dient als Grundmaterial. Er ist vielseitig kombinierbar. So entsteht Flexibilität. Ein sachlicher Abschluss folgt.

+

Absatz 138: Die Struktur ist robust und einfach. Sie trägt das Gesamtbild. Dadurch bleibt Ordnung. Ein kurzer Schluss beendet den Teil.

+

Absatz 139: Die Inhalte sind generisch und ruhig. Sie eignen sich für Tests. Das fördert die Anwendbarkeit. Ein Abschluss beschließt den Absatz.

+

Absatz 140: Die Sätze sind gut abgrenzbar. So lassen sie sich austauschen. Das erhöht den Nutzen. Ein ruhiger Schluss folgt.

+

Absatz 141: Der Abschnitt erhält die Gleichförmigkeit. Er vermeidet Brüche im Ton. So bleibt die Linie klar. Ein kurzer Abschluss beendet die Passage.

+

Absatz 142: Die Sprache ist sachlich distanziert. Dadurch bleibt der Blick frei. Das unterstützt viele Zwecke. Ein letzter Satz schließt ab.

+

Absatz 143: Der Text ist bewusst unkompliziert. Er enthält keine Nebenthemen. So wirkt er fokussiert. Ein sachlicher Abschluss folgt.

+

Absatz 144: Die Zeilen sind ordentlich strukturiert. Sie lassen sich leicht verarbeiten. Das ist für Beispiele hilfreich. Ein kurzer Schluss beendet den Teil.

+

Absatz 145: Der Abschnitt ist neutral und schlicht. Er passt in diverse Umgebungen. Dadurch ist er wiederverwendbar. Ein ruhiger Abschluss folgt.

+

Absatz 146: Die Sätze tragen gleichmäßige Informationen. Sie sind schnell erfassbar. Das erleichtert die Nutzung. Ein letzter Satz schließt ab.

+

Absatz 147: Die Wortwahl ist klar und direkt. Der Ton bleibt sachlich. So entsteht Zuverlässigkeit. Ein kurzer Abschluss beendet die Passage.

+

Absatz 148: Die Form ist neutral gehalten. Sie ist flexibel einsetzbar. Dadurch nimmt sie viele Rollen ein. Ein ruhiger Schluss folgt.

+

Absatz 149: Die Inhalte sind absichtlich einfach. Das erhöht die Verständlichkeit. Der Text bleibt angenehm lesbar. Ein abschließender Satz beschließt den Absatz.

+

Absatz 150: Die Sätze sind ausgewogen verteilt. Sie bauen keinen Druck auf. Das fördert ruhige Lektüre. Ein kurzer Abschluss folgt.

+

Absatz 151: Der Abschnitt knüpft an vorhandene Muster an. Er bleibt konsequent. So entsteht Kontinuität. Ein letzter Satz rundet ab.

+

Absatz 152: Die Sprache meidet Übertreibungen. Sie bleibt nüchtern. Das schafft Vertrauen. Ein sachlicher Abschluss folgt.

+

Absatz 153: Der Text ist praktisch orientiert. Er erfüllt eine Basisfunktion. Dadurch ist er nützlich. Ein kurzer Schluss beendet die Passage.

+

Absatz 154: Die Aussagen sind ordentlich gesetzt. Sie lassen sich sauber trennen. Das hilft bei Analysen. Ein ruhiger Abschluss folgt.

+

Absatz 155: Die Zeilen stärken die Gesamteinheit. Sie weichen nicht ab. So bleibt der Eindruck stabil. Ein Schlusssatz beschließt den Absatz.

+

Absatz 156: Der Ton ist klar und ruhig. Nichts stört den Fluss. Das erleichtert das Erfassen. Ein kurzer Abschluss folgt.

+

Absatz 157: Die Formulierungen sind universell gedacht. Sie schließen nichts aus. Dadurch bleiben sie offen. Ein letzter Satz rundet ab.

+

Absatz 158: Die Struktur ist bewusst repetitiv. Sie dient der Verlässlichkeit. Das hilft beim Testen. Ein sachlicher Abschluss folgt.

+

Absatz 159: Die Sätze sind inhaltlich unabhängig. Zusammen ergeben sie Ordnung. Das unterstützt den Zweck. Ein ruhiger Schlusssatz beendet den Teil.

+

Absatz 160: Der Abschnitt wirkt geordnet und still. Die Aussagen sind kontrolliert. Das fördert die Klarheit. Ein kurzer Abschluss beendet die Passage.

+

Absatz 161: Der Text bleibt schlicht und offen. Er folgt keiner Story. So bleibt er neutral. Ein letzter Satz beschließt den Absatz.

+

Absatz 162: Die Zeilen sind bewusst generisch formuliert. Dadurch sind sie stabil. Der Eindruck bleibt ruhig. Ein kurzer Schluss folgt.

+

Absatz 163: Die Sprache ist sachlich und unaufdringlich. Sie unterstützt die Aufgabe. Das macht sie geeignet. Ein abschließender Satz rundet ab.

+

Absatz 164: Die Struktur ist klar erkennbar. Sätze folgen aufeinander. So entsteht Überschaubarkeit. Ein sachlicher Abschluss folgt.

+

Absatz 165: Der Abschnitt wirkt gleichmäßig und nüchtern. Er bringt keine Überraschungen. Das stärkt die Einheit. Ein kurzer Schluss beendet die Passage.

+

Absatz 166: Die Aussagen sind schnell erfassbar. Sie sind gezielt einfach. Dadurch bleibt die Lesbarkeit hoch. Ein letzter Satz schließt ab.

+

Absatz 167: Der Text ist für Proben gedacht. Er ist frei gestaltbar. So lässt er sich gut einfügen. Ein ruhiger Abschluss folgt.

+

Absatz 168: Die Form ist klar und tragfähig. Sie hält die Ordnung. Das unterstützt viele Einsätze. Ein kurzer Schluss beendet den Teil.

+

Absatz 169: Die Sätze sind bewusst sachlich. Der Ton ist ungekünstelt. So entsteht Verlässlichkeit. Ein abschließender Satz folgt.

+

Absatz 170: Der Abschnitt orientiert sich am Schema. Er wahrt die Konsistenz. Dadurch bleibt der Eindruck gleich. Ein kurzer Abschluss schließt ab.

+

Absatz 171: Die Sprache bleibt neutral und still. Es gibt keine Ausschmückungen. Das betont die Funktion. Ein ruhiger Abschluss folgt.

+

Absatz 172: Die Zeilen sind gut kopierbar. Sie enthalten keine Sonderformen. So bleiben sie portabel. Ein letzter Satz beendet die Passage.

+

Absatz 173: Der Text erfüllt eine Platzhalterrolle. Er hält sich im Hintergrund. Dadurch unterstützt er andere Inhalte. Ein kurzer Schluss folgt.

+

Absatz 174: Die Struktur sichert Wiedererkennbarkeit. Das erleichtert Vergleiche. So bleibt alles kohärent. Ein abschließender Satz rundet ab.

+

Absatz 175: Die Aussagen sind knapp und sauber. Sie bilden kleine Einheiten. Das ist praktisch in der Anwendung. Ein kurzer Abschluss beendet den Absatz.

+

Absatz 176: Die Sprache ist ruhig und geordnet. Sie vermeidet Komplikationen. Dadurch bleibt der Lesefluss stabil. Ein letzter Satz schließt ab.

+

Absatz 177: Der Text bewahrt seine Neutralität. Er setzt keine Vorkenntnisse voraus. So ist er breit nutzbar. Ein sachlicher Abschluss folgt.

+

Absatz 178: Die Sätze sind aufgeräumt und klar. Sie lassen sich leicht kombinieren. Das unterstützt den Zweck. Ein ruhiger Schluss beendet den Teil.

+

Absatz 179: Der Abschnitt bleibt ohne thematische Tiefe. Er erfüllt eine technische Rolle. Dadurch ist er zielgerichtet. Ein kurzer Schlusssatz folgt.

+

Absatz 180: Die Form ist straff und schlicht. Sie lenkt nicht ab. Das hält die Aufmerksamkeit. Ein abschließender Satz beschließt die Passage.

+

Absatz 181: Die Zeilen ergänzen das Gesamtbild. Sie stützen das Muster. So bleibt alles planbar. Ein kurzer Abschluss folgt.

+

Absatz 182: Die Sprache ist nüchtern und präzise. Dadurch entsteht Klarheit. Der Text bleibt kontrolliert. Ein letzter Satz rundet ab.

+

Absatz 183: Der Abschnitt ist als Demo gedacht. Er ist bewusst allgemein. So passt er oft. Ein ruhiger Schluss beendet den Absatz.

+

Absatz 184: Die Sätze sind formstabil und ruhig. Sie tragen einfache Ideen. Das ist gut lesbar. Ein kurzer Abschluss folgt.

+

Absatz 185: Der Text ist leicht zu kopieren. Er hat keine versteckten Elemente. So bleibt er sauber. Ein abschließender Satz beendet die Passage.

+

Absatz 186: Die Struktur ist konstant und klar. Dadurch entsteht Verlässlichkeit. Der Zweck wird erfüllt. Ein kurzer Schluss schließt ab.

+

Absatz 187: Die Aussagen sind einzeln sinnvoll. Zusammen bilden sie Ordnung. Das hilft beim Einsatz. Ein ruhiger Abschluss folgt.

+

Absatz 188: Die Sprache meidet Komplexität. Sie bleibt zugänglich. So liest sich alles leicht. Ein letzter Satz beschließt den Absatz.

+

Absatz 189: Der Abschnitt hält die Form. Er bleibt ohne Ausschmückung. Das stärkt die Funktion. Ein kurzer Abschluss folgt.

+

Absatz 190: Die Sätze sind klar und knapp. Sie erfüllen eine Basisrolle. Dadurch sind sie nützlich. Ein ruhiger Schluss beendet die Passage.

+

Absatz 191: Der Text ist bewusst gleichmäßig. Er vermeidet Brüche. So wirkt er geschlossen. Ein letzter Satz rundet ab.

+

Absatz 192: Die Zeilen tragen neutrale Inhalte. Sie sind leicht kombinierbar. Das erhöht die Flexibilität. Ein kurzer Abschluss folgt.

+

Absatz 193: Die Sprache ist kontrolliert gewählt. Sie führt ohne Umwege. So bleibt alles klar. Ein abschließender Satz beendet den Absatz.

+

Absatz 194: Der Abschnitt ordnet sich dem Ziel unter. Er bleibt unauffällig. Dadurch ist er vielseitig. Ein kurzer Schluss schließt ab.

+

Absatz 195: Die Sätze sind leicht verständlich. Sie bilden eine ruhige Abfolge. Das unterstützt die Anwendung. Ein ruhiger Abschluss folgt.

+

Absatz 196: Der Text hat keine Stilmittel. Er ist funktional geschrieben. So bleibt er rein. Ein letzter Satz beendet die Passage.

+

Absatz 197: Die Struktur ist wiedererkennbar. Sie erleichtert Tests. Dadurch wird der Zweck erfüllt. Ein kurzer Abschluss folgt.

+

Absatz 198: Die Aussagen sind neutral und offen. Sie greifen nichts vorweg. Das bietet Spielraum. Ein abschließender Satz beschließt den Absatz.

+

Absatz 199: Der Abschnitt bleibt klar und schlicht. Es gibt keine Besonderheiten. So passt er in viele Muster. Ein kurzer Schluss beendet die Passage.

+

Absatz 200: Die Zeilen runden den Umfang ab. Sie bleiben dem Ton treu. Dadurch wirkt das Ganze kohärent. Ein ruhiger Abschlusssatz beendet das Dokument.

+ + diff --git a/test/library/test_history.cpp b/test/library/test_history.cpp index 1763ae1..2b39492 100644 --- a/test/library/test_history.cpp +++ b/test/library/test_history.cpp @@ -32,10 +32,6 @@ void HistoryTest::test_creation() QCOMPARE( browser->backwardHistoryCount(), 0 ); QCOMPARE( browser->isBackwardAvailable(), false ); QCOMPARE( browser->isForwardAvailable(), false ); - for ( int idx = -3; idx < 3; idx++ ) - { - QCOMPARE( browser->historyUrl( idx ), QUrl() ); - } // no url set, should not crash browser->home(); } @@ -49,10 +45,6 @@ void HistoryTest::test_home() QCOMPARE( browser->backwardHistoryCount(), 0 ); QCOMPARE( browser->isBackwardAvailable(), false ); QCOMPARE( browser->isForwardAvailable(), false ); - for ( int idx = -3; idx < 3; idx++ ) - { - QCOMPARE( browser->historyUrl( idx ), QUrl() ); - } } void HistoryTest::test_history() diff --git a/test/library/test_search.cpp b/test/library/test_search.cpp new file mode 100644 index 0000000..7581bbd --- /dev/null +++ b/test/library/test_search.cpp @@ -0,0 +1,79 @@ +#include "test_search.h" +#include + +#include +#include + +int main( int argc, char** argv ) +{ + QApplication app( argc, argv ); + + SearchTest mContentTest; + return QTest::qExec( &mContentTest, mContentTest.args() ); +} + +QLiteHtmlBrowser* SearchTest::createMainWindow( const QSize& size ) +{ + mWnd = std::make_unique(); + auto browser = new QLiteHtmlBrowser( nullptr ); + mWnd->setCentralWidget( browser ); + browser->setMinimumSize( size ); + browser->update(); + browser->show(); + mWnd->show(); + return browser; +} + +void SearchTest::test_creation() +{ + auto browser = createMainWindow( mBrowserSize ); + browser->home(); +} + +void SearchTest::test_search_single_word() +{ + auto browser = createMainWindow( mBrowserSize ); + // QVERIFY( mWnd->centralWidget() ); + + QCOMPARE( browser->forwardHistoryCount(), 0 ); + QCOMPARE( browser->backwardHistoryCount(), 0 ); + auto base = QString{ TEST_SOURCE_DIR } + "/search/"; + + browser->setSource( QUrl::fromLocalFile( base + "/LongGermanText.html" ) ); + auto found = browser->searchText( QStringLiteral( "Sätze" ) ); + QCOMPARE( found, 40 ); + + found = browser->searchText( " Er" ); + QCOMPARE( found, 46 ); +} + +void SearchTest::test_search_phrase() +{ + auto browser = createMainWindow( mBrowserSize ); + // QVERIFY( mWnd->centralWidget() ); + + QCOMPARE( browser->forwardHistoryCount(), 0 ); + QCOMPARE( browser->backwardHistoryCount(), 0 ); + auto base = QString{ TEST_SOURCE_DIR } + "/search/"; + + browser->setSource( QUrl::fromLocalFile( base + "/LongGermanText.html" ) ); + auto found = browser->searchText( "allgemeine Beobachtungen" ); + QCOMPARE( found, 1 ); + + found = browser->searchText( "schliesst ab" ); + QCOMPARE( found, 0 ); +} + +void SearchTest::test_search_phrase_multi_element() +{ + auto browser = createMainWindow( mBrowserSize ); + // QVERIFY( mWnd->centralWidget() ); + + QCOMPARE( browser->forwardHistoryCount(), 0 ); + QCOMPARE( browser->backwardHistoryCount(), 0 ); + auto base = QString{ TEST_SOURCE_DIR } + "/search/"; + + browser->setSource( QUrl::fromLocalFile( base + "/LongGermanText.html" ) ); + auto found = browser->searchText( QStringLiteral( "schließt ab. Absatz 48" ) ); + QCOMPARE( found, 1 ); +} diff --git a/test/library/test_search.h b/test/library/test_search.h new file mode 100644 index 0000000..e1c3590 --- /dev/null +++ b/test/library/test_search.h @@ -0,0 +1,33 @@ + +#include +#include +#include "test_base.h" + +class QLiteHtmlBrowser; + +class SearchTest : public TestBase +{ + Q_OBJECT +public: + SearchTest() + : TestBase( qApp->arguments() ) {}; + virtual ~SearchTest() = default; + +private Q_SLOTS: + void init() { TestBase::init(); } + void cleanup() + { + TestBase::cleanup(); + mWnd.reset(); + mWnd = nullptr; + } + void test_creation(); + void test_search_single_word(); + void test_search_phrase(); + void test_search_phrase_multi_element(); + +private: + QLiteHtmlBrowser* createMainWindow( const QSize& ); + std::unique_ptr mWnd = nullptr; + QSize mBrowserSize = { 800, 600 }; +}; From 47a8c4101f39257b84dc8eaab4f3bf63f31925f1 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Wed, 22 Oct 2025 09:48:52 +0200 Subject: [PATCH 03/11] fix scaling issues --- src/container_qt.cpp | 20 ++++++++++++++++++-- src/container_qt.h | 4 ++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/container_qt.cpp b/src/container_qt.cpp index ccdb9a3..00f8c08 100644 --- a/src/container_qt.cpp +++ b/src/container_qt.cpp @@ -908,6 +908,7 @@ void container_qt::setScale( double scale ) auto relV = ( 0 < verticalScrollBar()->maximum() ) ? static_cast( verticalScrollBar()->value() ) / verticalScrollBar()->maximum() : 0.0; mScale = std::clamp( scale, mMinScale, mMaxScale ); render(); + searchText( mSearchTerm ); horizontalScrollBar()->setValue( std::floor( relH * horizontalScrollBar()->maximum() ) ); verticalScrollBar()->setValue( std::floor( relV * verticalScrollBar()->maximum() ) ); } @@ -999,6 +1000,20 @@ int container_qt::inv_scaled( int i ) const return std::floor( i * mScale ); } +QSize container_qt::inv_scaled( const QSize& size ) const +{ + return QSize( std::floor( size.width() * mScale ), std::floor( size.height() * mScale ) ); +} + +QPoint container_qt::inv_scaled( const QPoint& point ) const +{ + return QPoint( std::floor( point.x() * mScale ), std::floor( point.y() * mScale ) ); +} +QRect container_qt::inv_scaled( const QRect& rect ) const +{ + return QRect( inv_scaled( rect.topLeft() ), inv_scaled( rect.size() ) ); +} + void container_qt::mouseMoveEvent( QMouseEvent* e ) { @@ -1104,7 +1119,8 @@ void container_qt::print( QPagedPaintDevice* paintDevice ) int container_qt::searchText( const QString& text ) { - search_text( mDocument, text.toStdString(), true ); + mSearchTerm = text; + search_text( mDocument, mSearchTerm.toStdString(), true ); viewport()->update(); auto* result = get_current_result(); @@ -1388,7 +1404,7 @@ void container_qt::highlight_text_at_position( litehtml::uint_ptr hdc, const lit auto scroll_pos = -scrollBarPos(); - p->drawRect( pos.x + scroll_pos.x(), pos.y + scroll_pos.y(), pos.width, pos.height ); + p->drawRect( ( QRect( pos.x + scroll_pos.x(), pos.y + scroll_pos.y(), pos.width, pos.height ) ) ); p->restore(); } diff --git a/src/container_qt.h b/src/container_qt.h index d9aff44..a26d16d 100644 --- a/src/container_qt.h +++ b/src/container_qt.h @@ -94,8 +94,11 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co QRect scaled( const QRect& rect ) const; QPoint scaled( const QPoint& point ) const; int scaled( int i ) const; + QPoint inv_scaled( const QPoint& point ) const; int inv_scaled( int i ) const; + QSize inv_scaled( const QSize& size ) const; QPixmap load_image_data( const QUrl& url ); + QRect inv_scaled( const QRect& rect ) const; QPixmap load_pixmap( const QUrl& url ); QUrl resolveUrl( const char* src, const char* baseurl ) const; void render(); @@ -164,4 +167,5 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co std::vector m_search_results = {}; int m_current_result_index = -1; const QColor mHighlightColor = QColor( 255, 255, 0, 30 ); + QString mSearchTerm; }; From a6811486a481fc74909ba97f5037dbb6fa70621c Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Wed, 22 Oct 2025 09:54:11 +0200 Subject: [PATCH 04/11] fix: clear search if no search term is given --- src/QLiteHtmlBrowserImpl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/QLiteHtmlBrowserImpl.cpp b/src/QLiteHtmlBrowserImpl.cpp index 71d8bcb..b24ea9a 100644 --- a/src/QLiteHtmlBrowserImpl.cpp +++ b/src/QLiteHtmlBrowserImpl.cpp @@ -453,7 +453,7 @@ void QLiteHtmlBrowserImpl::print( QPagedPaintDevice* printer ) const int QLiteHtmlBrowserImpl::searchText( const QString& phrase ) { auto ret = 0; - if ( mContainer && !phrase.isEmpty() ) + if ( mContainer ) { ret = mContainer->searchText( phrase ); } From 967d8daf70b66acdeed719359b77da5b954b8a6c Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Wed, 22 Oct 2025 10:05:15 +0200 Subject: [PATCH 05/11] [] test_search process render events --- test/library/test_search.cpp | 21 +++++++++++---------- test/library/test_search.h | 1 - 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/test/library/test_search.cpp b/test/library/test_search.cpp index 7581bbd..28333c0 100644 --- a/test/library/test_search.cpp +++ b/test/library/test_search.cpp @@ -24,12 +24,6 @@ QLiteHtmlBrowser* SearchTest::createMainWindow( const QSize& size ) return browser; } -void SearchTest::test_creation() -{ - auto browser = createMainWindow( mBrowserSize ); - browser->home(); -} - void SearchTest::test_search_single_word() { auto browser = createMainWindow( mBrowserSize ); @@ -40,11 +34,17 @@ void SearchTest::test_search_single_word() auto base = QString{ TEST_SOURCE_DIR } + "/search/"; browser->setSource( QUrl::fromLocalFile( base + "/LongGermanText.html" ) ); - auto found = browser->searchText( QStringLiteral( "Sätze" ) ); + auto found = browser->searchText( ( "Sätze" ) ); + qApp->processEvents(); QCOMPARE( found, 40 ); - found = browser->searchText( " Er" ); + found = browser->searchText( ( " Er" ) ); + qApp->processEvents(); QCOMPARE( found, 46 ); + + found = browser->searchText( "" ); + qApp->processEvents(); + QCOMPARE( found, 0 ); } void SearchTest::test_search_phrase() @@ -58,9 +58,11 @@ void SearchTest::test_search_phrase() browser->setSource( QUrl::fromLocalFile( base + "/LongGermanText.html" ) ); auto found = browser->searchText( "allgemeine Beobachtungen" ); + qApp->processEvents(); QCOMPARE( found, 1 ); found = browser->searchText( "schliesst ab" ); + qApp->processEvents(); QCOMPARE( found, 0 ); } @@ -69,11 +71,10 @@ void SearchTest::test_search_phrase_multi_element() auto browser = createMainWindow( mBrowserSize ); // QVERIFY( mWnd->centralWidget() ); - QCOMPARE( browser->forwardHistoryCount(), 0 ); - QCOMPARE( browser->backwardHistoryCount(), 0 ); auto base = QString{ TEST_SOURCE_DIR } + "/search/"; browser->setSource( QUrl::fromLocalFile( base + "/LongGermanText.html" ) ); auto found = browser->searchText( QStringLiteral( "schließt ab. Absatz 48" ) ); + qApp->processEvents(); QCOMPARE( found, 1 ); } diff --git a/test/library/test_search.h b/test/library/test_search.h index e1c3590..d69cfd4 100644 --- a/test/library/test_search.h +++ b/test/library/test_search.h @@ -21,7 +21,6 @@ private Q_SLOTS: mWnd.reset(); mWnd = nullptr; } - void test_creation(); void test_search_single_word(); void test_search_phrase(); void test_search_phrase_multi_element(); From 6afe68eb4ec984be6a108c868e589c0217e05cc4 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Wed, 22 Oct 2025 10:10:44 +0200 Subject: [PATCH 06/11] [] apply clang-format --- src/QLiteHtmlBrowser.cpp | 1 - src/QLiteHtmlBrowserImpl.cpp | 1 - src/QLiteHtmlBrowserImpl.h | 6 +++--- src/container_qt.cpp | 2 +- src/container_qt.h | 38 ++++++++++++++++++------------------ test/browser/testbrowser.cpp | 6 +++--- test/browser/testbrowser.h | 4 ++-- test/library/test_search.h | 2 +- 8 files changed, 29 insertions(+), 31 deletions(-) diff --git a/src/QLiteHtmlBrowser.cpp b/src/QLiteHtmlBrowser.cpp index 548939b..8a95628 100644 --- a/src/QLiteHtmlBrowser.cpp +++ b/src/QLiteHtmlBrowser.cpp @@ -130,7 +130,6 @@ void QLiteHtmlBrowser::clearHistory() mImpl->clearHistory(); } - int QLiteHtmlBrowser::backwardHistoryCount() const { return mImpl->backwardHistoryCount(); diff --git a/src/QLiteHtmlBrowserImpl.cpp b/src/QLiteHtmlBrowserImpl.cpp index b24ea9a..6ee1fdd 100644 --- a/src/QLiteHtmlBrowserImpl.cpp +++ b/src/QLiteHtmlBrowserImpl.cpp @@ -407,7 +407,6 @@ void QLiteHtmlBrowserImpl::setOpenExternalLinks( bool open ) } } - void QLiteHtmlBrowserImpl::forward() { if ( !mFWHistStack.isEmpty() ) diff --git a/src/QLiteHtmlBrowserImpl.h b/src/QLiteHtmlBrowserImpl.h index 66a8926..0ff02c8 100644 --- a/src/QLiteHtmlBrowserImpl.h +++ b/src/QLiteHtmlBrowserImpl.h @@ -58,13 +58,13 @@ class QLiteHtmlBrowserImpl : public QWidget void backward(); void reload(); - int backwardHistoryCount() const { return ( 1 < mBWHistStack.count() ) ? mBWHistStack.count() - 1 : 0; } - int forwardHistoryCount() const { return ( 0 < mFWHistStack.count() ) ? mFWHistStack.count() : 0; } + int backwardHistoryCount() const { return ( 1 < mBWHistStack.count() ) ? mBWHistStack.count() - 1 : 0; } + int forwardHistoryCount() const { return ( 0 < mFWHistStack.count() ) ? mFWHistStack.count() : 0; } const QString& caption() const; void print( QPagedPaintDevice* printer ) const; - int searchText( const QString& ); // search for given string and return number of found results + int searchText( const QString& ); // search for given string and return number of found results void nextSearchResult(); void previousSearchResult(); diff --git a/src/container_qt.cpp b/src/container_qt.cpp index 00f8c08..f89fe34 100644 --- a/src/container_qt.cpp +++ b/src/container_qt.cpp @@ -1379,7 +1379,7 @@ void container_qt::draw_highlights( litehtml::uint_ptr hdc ) { for ( auto it = m_search_results.begin(); it != m_search_results.end(); ++it ) { - const auto result = ( *it ); + const auto result = ( *it ); for ( auto it = result.fragments.begin(); it != result.fragments.end(); ++it ) { litehtml::position pos = ( *it ).pos; diff --git a/src/container_qt.h b/src/container_qt.h index a26d16d..5936baf 100644 --- a/src/container_qt.h +++ b/src/container_qt.h @@ -126,24 +126,24 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co litehtml::position bounding_box; // bounding box over all elements }; - int search_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive = true ); - bool next_search_result(); - bool previous_search_result(); - const TextSearchResult* get_current_result() const; - void highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const std::string& text ); - void draw_highlights( litehtml::uint_ptr hdc ); - void clear_highlights() { m_search_results.clear(); } - std::string normalizeWhitespace( const std::string& text ); - void searchTextInDocument( litehtml::document::ptr doc, - const std::string& search_term, - std::vector& results, - bool case_sensitive = true ); - void collectTextFragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText ); - litehtml::position calculatePreciseBoundingBox( const std::vector& allFragments, - int searchStart, - int searchEnd, - std::vector& matchedFragments ); - void scrollToSearchResult( const TextSearchResult* ); + int search_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive = true ); + bool next_search_result(); + bool previous_search_result(); + const TextSearchResult* get_current_result() const; + void highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const std::string& text ); + void draw_highlights( litehtml::uint_ptr hdc ); + void clear_highlights() { m_search_results.clear(); } + std::string normalizeWhitespace( const std::string& text ); + void searchTextInDocument( litehtml::document::ptr doc, + const std::string& search_term, + std::vector& results, + bool case_sensitive = true ); + void collectTextFragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText ); + litehtml::position calculatePreciseBoundingBox( const std::vector& allFragments, + int searchStart, + int searchEnd, + std::vector& matchedFragments ); + void scrollToSearchResult( const TextSearchResult* ); std::shared_ptr mDocument; QByteArray mDocumentSource; @@ -163,7 +163,7 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co QString mMasterCSS; QString mUserCSS; QStack mClipStack; - litehtml::position mClip = {}; + litehtml::position mClip = {}; std::vector m_search_results = {}; int m_current_result_index = -1; const QColor mHighlightColor = QColor( 255, 255, 0, 30 ); diff --git a/test/browser/testbrowser.cpp b/test/browser/testbrowser.cpp index c576793..504ea81 100644 --- a/test/browser/testbrowser.cpp +++ b/test/browser/testbrowser.cpp @@ -206,9 +206,9 @@ void TestBrowser::export2pdf() } void TestBrowser::searchText( const QString& text ) { - auto found = mBrowser->searchText( text ); - mPreviousSearchResult->setEnabled( found > 0 ); - mNextSearchResult->setEnabled( found > 0 ); + auto found = mBrowser->searchText( text ); + mPreviousSearchResult->setEnabled( found > 0 ); + mNextSearchResult->setEnabled( found > 0 ); } void TestBrowser::previousSearchResult() diff --git a/test/browser/testbrowser.h b/test/browser/testbrowser.h index ad2a01c..c9bfb74 100644 --- a/test/browser/testbrowser.h +++ b/test/browser/testbrowser.h @@ -43,10 +43,10 @@ class TestBrowser : public QMainWindow QHelpBrowser* mBrowser; QMenuBar mMenu; QDir mLastDirectory; - QLineEdit* mUrl = nullptr; + QLineEdit* mUrl = nullptr; QLineEdit* mSearchText = nullptr; QToolBar mToolBar; - QHelpEngine* mHelpEngine = nullptr; + QHelpEngine* mHelpEngine = nullptr; QAction* mNextSearchResult = nullptr; QAction* mPreviousSearchResult = nullptr; }; diff --git a/test/library/test_search.h b/test/library/test_search.h index d69cfd4..01613d2 100644 --- a/test/library/test_search.h +++ b/test/library/test_search.h @@ -10,7 +10,7 @@ class SearchTest : public TestBase Q_OBJECT public: SearchTest() - : TestBase( qApp->arguments() ) {}; + : TestBase( qApp->arguments() ){}; virtual ~SearchTest() = default; private Q_SLOTS: From 77ad248333641d137139560f89753def4eb391dc Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Wed, 22 Oct 2025 10:14:13 +0200 Subject: [PATCH 07/11] [] apply clang-format --- include/qlitehtmlbrowser/QLiteHtmlBrowser.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h index 96f98a0..3fa5f8d 100644 --- a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h +++ b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h @@ -125,11 +125,11 @@ class QLITEHTMLBROWSER_EXPORT QLiteHtmlBrowser : public QWidget /// set file system paths where relative urls or resources are resolved void setSearchPaths( const QStringList& ); - bool isBackwardAvailable() const; - bool isForwardAvailable() const; - void clearHistory(); - int backwardHistoryCount() const; - int forwardHistoryCount() const; + bool isBackwardAvailable() const; + bool isForwardAvailable() const; + void clearHistory(); + int backwardHistoryCount() const; + int forwardHistoryCount() const; /// return title for url if available, else returns empty string const QString& caption() const; @@ -138,7 +138,7 @@ class QLITEHTMLBROWSER_EXPORT QLiteHtmlBrowser : public QWidget void print( QPagedPaintDevice* printer ) const; /// search text in document - int searchText( const QString& ); + int searchText( const QString& ); void scrollToNextSearchResult(); void scrollToPreviousSearchResult(); From 99efa70b720bdfc8ffaa0468935dcfedf9d17ce7 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Wed, 22 Oct 2025 11:40:27 +0200 Subject: [PATCH 08/11] make highlight color configurable --- include/qlitehtmlbrowser/QLiteHtmlBrowser.h | 10 +++++++--- src/QLiteHtmlBrowser.cpp | 9 +++++++++ src/QLiteHtmlBrowserImpl.cpp | 20 +++++++++++++++++++- src/QLiteHtmlBrowserImpl.h | 2 ++ src/container_qt.h | 4 +++- 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h index 3fa5f8d..d6eedf7 100644 --- a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h +++ b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h @@ -45,6 +45,8 @@ class QLITEHTMLBROWSER_EXPORT QLiteHtmlBrowser : public QWidget /// Set additional paths to resolve relative urls or resources. These paths are considered /// if the url could not get resolved against the given base url. Q_PROPERTY( QStringList searchPaths READ searchPaths WRITE setSearchPaths ) + /// configure highlight color e.g. for search results + Q_PROPERTY( QColor highlightColor READ highlightColor WRITE setHighlightColor DESIGNABLE true ) public: /// identifier for the type of resource that should get loaded as hint @@ -138,9 +140,11 @@ class QLITEHTMLBROWSER_EXPORT QLiteHtmlBrowser : public QWidget void print( QPagedPaintDevice* printer ) const; /// search text in document - int searchText( const QString& ); - void scrollToNextSearchResult(); - void scrollToPreviousSearchResult(); + int searchText( const QString& ); + void scrollToNextSearchResult(); + void scrollToPreviousSearchResult(); + QColor highlightColor() const; + void setHighlightColor( QColor color ); public Q_SLOTS: /// set URL to given url. The URL may be an url to local file, QtHelp, http etc. diff --git a/src/QLiteHtmlBrowser.cpp b/src/QLiteHtmlBrowser.cpp index 8a95628..feec062 100644 --- a/src/QLiteHtmlBrowser.cpp +++ b/src/QLiteHtmlBrowser.cpp @@ -177,3 +177,12 @@ void QLiteHtmlBrowser::scrollToPreviousSearchResult() { mImpl->previousSearchResult(); } + +QColor QLiteHtmlBrowser::highlightColor() const +{ + return mImpl->highlightColor(); +} +void QLiteHtmlBrowser::setHighlightColor( QColor color ) +{ + mImpl->setHighlightColor( color ); +} diff --git a/src/QLiteHtmlBrowserImpl.cpp b/src/QLiteHtmlBrowserImpl.cpp index 6ee1fdd..6fef2e1 100644 --- a/src/QLiteHtmlBrowserImpl.cpp +++ b/src/QLiteHtmlBrowserImpl.cpp @@ -62,6 +62,24 @@ void QLiteHtmlBrowserImpl::setCSS( const QString& css ) applyCSS(); } +void QLiteHtmlBrowserImpl::setHighlightColor( QColor color ) +{ + if ( mContainer ) + { + mContainer->setHighlightColor( color ); + } +} + +QColor QLiteHtmlBrowserImpl::highlightColor() const +{ + QColor color; + if ( mContainer ) + { + color = mContainer->highlightColor(); + } + return color; +} + QString QLiteHtmlBrowserImpl::readResourceCss( const QString& resource ) const { QString css; @@ -270,7 +288,7 @@ double QLiteHtmlBrowserImpl::scale() const return scale; } -QByteArray QLiteHtmlBrowserImpl::loadResource( int type, const QUrl& url ) +QByteArray QLiteHtmlBrowserImpl::loadResource( int /*type*/, const QUrl& url ) { QByteArray data; diff --git a/src/QLiteHtmlBrowserImpl.h b/src/QLiteHtmlBrowserImpl.h index 0ff02c8..44d0ef3 100644 --- a/src/QLiteHtmlBrowserImpl.h +++ b/src/QLiteHtmlBrowserImpl.h @@ -67,6 +67,8 @@ class QLiteHtmlBrowserImpl : public QWidget int searchText( const QString& ); // search for given string and return number of found results void nextSearchResult(); void previousSearchResult(); + QColor highlightColor() const; + void setHighlightColor( QColor color ); protected: void changeEvent( QEvent* ) override; diff --git a/src/container_qt.h b/src/container_qt.h index 5936baf..0f3f19f 100644 --- a/src/container_qt.h +++ b/src/container_qt.h @@ -34,6 +34,8 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co int searchText( const QString& text ); void scrollToNextSearchResult(); void scrollToPreviousSearchResult(); + void setHighlightColor( const QColor& color ) { mHighlightColor = color; } + QColor highlightColor() const { return mHighlightColor; } protected: void paintEvent( QPaintEvent* ) override; @@ -166,6 +168,6 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co litehtml::position mClip = {}; std::vector m_search_results = {}; int m_current_result_index = -1; - const QColor mHighlightColor = QColor( 255, 255, 0, 30 ); + QColor mHighlightColor = QColor( 255, 255, 0, 30 ); QString mSearchTerm; }; From 0ddc251df6c3080fcf5d7635e630cce6a77856f6 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Wed, 22 Oct 2025 11:43:52 +0200 Subject: [PATCH 09/11] fix clang-format --- src/QLiteHtmlBrowserImpl.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/QLiteHtmlBrowserImpl.h b/src/QLiteHtmlBrowserImpl.h index 44d0ef3..1430ecc 100644 --- a/src/QLiteHtmlBrowserImpl.h +++ b/src/QLiteHtmlBrowserImpl.h @@ -64,9 +64,9 @@ class QLiteHtmlBrowserImpl : public QWidget const QString& caption() const; void print( QPagedPaintDevice* printer ) const; - int searchText( const QString& ); // search for given string and return number of found results - void nextSearchResult(); - void previousSearchResult(); + int searchText( const QString& ); // search for given string and return number of found results + void nextSearchResult(); + void previousSearchResult(); QColor highlightColor() const; void setHighlightColor( QColor color ); From bc8c5f1f8f4e47ecd3685b260028915ee56a6031 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Thu, 23 Oct 2025 13:11:50 +0200 Subject: [PATCH 10/11] replace search with find --- include/qlitehtmlbrowser/QLiteHtmlBrowser.h | 10 +- src/QLiteHtmlBrowser.cpp | 12 +- src/QLiteHtmlBrowserImpl.cpp | 12 +- src/QLiteHtmlBrowserImpl.h | 6 +- src/container_qt.cpp | 183 +++++++----------- src/container_qt.h | 44 ++--- test/browser/testbrowser.cpp | 42 ++-- test/browser/testbrowser.h | 16 +- test/library/CMakeLists.txt | 2 +- .../{test_search.cpp => test_find.cpp} | 24 +-- test/library/{test_search.h => test_find.h} | 12 +- 11 files changed, 166 insertions(+), 197 deletions(-) rename test/library/{test_search.cpp => test_find.cpp} (74%) rename test/library/{test_search.h => test_find.h} (72%) diff --git a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h index d6eedf7..9abd409 100644 --- a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h +++ b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h @@ -139,10 +139,12 @@ class QLITEHTMLBROWSER_EXPORT QLiteHtmlBrowser : public QWidget /// print document into paged paint device like a printer or pdf void print( QPagedPaintDevice* printer ) const; - /// search text in document - int searchText( const QString& ); - void scrollToNextSearchResult(); - void scrollToPreviousSearchResult(); + /// find text in document + int findText( const QString& ); + void findNextMatch(); + void findPreviousMatch(); + + /// configure interface for highlight color, i.e. to highlight found text QColor highlightColor() const; void setHighlightColor( QColor color ); diff --git a/src/QLiteHtmlBrowser.cpp b/src/QLiteHtmlBrowser.cpp index feec062..36bdc18 100644 --- a/src/QLiteHtmlBrowser.cpp +++ b/src/QLiteHtmlBrowser.cpp @@ -163,19 +163,19 @@ void QLiteHtmlBrowser::print( QPagedPaintDevice* printer ) const } } -int QLiteHtmlBrowser::searchText( const QString& text ) +int QLiteHtmlBrowser::findText( const QString& text ) { - return mImpl->searchText( text ); + return mImpl->findText( text ); } -void QLiteHtmlBrowser::scrollToNextSearchResult() +void QLiteHtmlBrowser::findNextMatch() { - mImpl->nextSearchResult(); + mImpl->nextFindMatch(); } -void QLiteHtmlBrowser::scrollToPreviousSearchResult() +void QLiteHtmlBrowser::findPreviousMatch() { - mImpl->previousSearchResult(); + mImpl->previousFindMatch(); } QColor QLiteHtmlBrowser::highlightColor() const diff --git a/src/QLiteHtmlBrowserImpl.cpp b/src/QLiteHtmlBrowserImpl.cpp index 6fef2e1..c5380de 100644 --- a/src/QLiteHtmlBrowserImpl.cpp +++ b/src/QLiteHtmlBrowserImpl.cpp @@ -467,29 +467,29 @@ void QLiteHtmlBrowserImpl::print( QPagedPaintDevice* printer ) const } } -int QLiteHtmlBrowserImpl::searchText( const QString& phrase ) +int QLiteHtmlBrowserImpl::findText( const QString& phrase ) { auto ret = 0; if ( mContainer ) { - ret = mContainer->searchText( phrase ); + ret = mContainer->findText( phrase ); } return ret; } -void QLiteHtmlBrowserImpl::nextSearchResult() +void QLiteHtmlBrowserImpl::nextFindMatch() { if ( mContainer ) { - mContainer->scrollToNextSearchResult(); + mContainer->findNextMatch(); } } -void QLiteHtmlBrowserImpl::previousSearchResult() +void QLiteHtmlBrowserImpl::previousFindMatch() { if ( mContainer ) { - mContainer->scrollToPreviousSearchResult(); + mContainer->findPreviousMatch(); } } diff --git a/src/QLiteHtmlBrowserImpl.h b/src/QLiteHtmlBrowserImpl.h index 1430ecc..73a22bb 100644 --- a/src/QLiteHtmlBrowserImpl.h +++ b/src/QLiteHtmlBrowserImpl.h @@ -64,9 +64,9 @@ class QLiteHtmlBrowserImpl : public QWidget const QString& caption() const; void print( QPagedPaintDevice* printer ) const; - int searchText( const QString& ); // search for given string and return number of found results - void nextSearchResult(); - void previousSearchResult(); + int findText( const QString& ); // search for given string and return number of found matches + void nextFindMatch(); + void previousFindMatch(); QColor highlightColor() const; void setHighlightColor( QColor color ); diff --git a/src/container_qt.cpp b/src/container_qt.cpp index f89fe34..6453e58 100644 --- a/src/container_qt.cpp +++ b/src/container_qt.cpp @@ -30,44 +30,11 @@ static const auto CSS_GENERIC_FONT_TO_QFONT_STYLEHINT = QMap #include #include #include #include -// // Textsuche durchführen (case-sensitive) -// std::string search_term = "Beispiel"; -// int result_count = container.search_text(doc, search_term, true); - -// std::cout << "Gefunden: " << result_count << " Vorkommen von '" -// << search_term << "'" << std::endl; - -// // Durch alle Ergebnisse iterieren -// const auto& all_results = container.get_all_results(); -// for (size_t i = 0; i < all_results.size(); ++i) { -// const auto& result = all_results[i]; -// std::cout << "Treffer " << (i + 1) << ":" << std::endl; -// std::cout << " Text: '" << result.matched_text << "'" << std::endl; -// std::cout << " Position: x=" << result.element_pos.x -// << ", y=" << result.element_pos.y << std::endl; -// std::cout << " Größe: " << result.element_pos.width -// << "x" << result.element_pos.height << std::endl; -// std::cout << " Offset: " << result.char_offset << std::endl; -// } - -// // Navigation durch Suchergebnisse -// container.next_search_result(); -// const TextSearchResult* current = container.get_current_result(); -// if (current) { -// std::cout << "\nAktueller Treffer an Position: (" -// << current->element_pos.x << ", " -// << current->element_pos.y << ")" << std::endl; -// } - -// return 0; -// } - container_qt::container_qt( QWidget* parent ) : QAbstractScrollArea( parent ) { @@ -621,7 +588,7 @@ Qt::PenStyle container_qt::toPenStyle( const litehtml::border_style& style ) con } return p; } -void container_qt::draw_borders( litehtml::uint_ptr hdc, const litehtml::borders& borders, const litehtml::position& draw_pos, bool root ) +void container_qt::draw_borders( litehtml::uint_ptr hdc, const litehtml::borders& borders, const litehtml::position& draw_pos, bool /*root*/ ) { QPainter* p( reinterpret_cast( hdc ) ); p->save(); @@ -787,8 +754,8 @@ void container_qt::set_base_url( const char* base_url ) mBaseUrl = base_url; } -void container_qt::link( const std::shared_ptr& doc, const litehtml::element::ptr& el ) {} -void container_qt::on_anchor_click( const char* url, const litehtml::element::ptr& el ) +void container_qt::link( const std::shared_ptr& /*doc*/, const litehtml::element::ptr& /*el*/ ) {} +void container_qt::on_anchor_click( const char* url, const litehtml::element::ptr& /*el*/ ) { if ( mDocument ) { @@ -868,7 +835,7 @@ void container_qt::import_css( litehtml::string& text, const litehtml::string& u text = QString::fromUtf8( content.constData() ).toStdString(); } } -void container_qt::set_clip( const litehtml::position& pos, const litehtml::border_radiuses& bdr_radius /*,bool valid_x, bool valid_y */ ) +void container_qt::set_clip( const litehtml::position& pos, const litehtml::border_radiuses& /*bdr_radius*/ /*,bool valid_x, bool valid_y */ ) { mClipStack.push( pos ); mClip = pos; @@ -895,8 +862,9 @@ void container_qt::get_client_rect( litehtml::position& client ) const client.y = contentsMargins().top(); } -std::shared_ptr -container_qt::create_element( const char* tag_name, const litehtml::string_map& attributes, const std::shared_ptr& doc ) +std::shared_ptr container_qt::create_element( const char* /*tag_name*/, + const litehtml::string_map& /*attributes*/, + const std::shared_ptr& /*doc*/ ) { return {}; } @@ -908,7 +876,7 @@ void container_qt::setScale( double scale ) auto relV = ( 0 < verticalScrollBar()->maximum() ) ? static_cast( verticalScrollBar()->value() ) / verticalScrollBar()->maximum() : 0.0; mScale = std::clamp( scale, mMinScale, mMaxScale ); render(); - searchText( mSearchTerm ); + findText( mFindText ); horizontalScrollBar()->setValue( std::floor( relH * horizontalScrollBar()->maximum() ) ); verticalScrollBar()->setValue( std::floor( relV * verticalScrollBar()->maximum() ) ); } @@ -1117,48 +1085,48 @@ void container_qt::print( QPagedPaintDevice* paintDevice ) del_clip(); } -int container_qt::searchText( const QString& text ) +int container_qt::findText( const QString& text ) { - mSearchTerm = text; - search_text( mDocument, mSearchTerm.toStdString(), true ); + mFindText = text; + find_text( mDocument, mFindText.toStdString(), true ); viewport()->update(); - auto* result = get_current_result(); - if ( result ) + auto* match = find_current_match(); + if ( match ) { - scrollToSearchResult( result ); + scroll_to_find_match( match ); } - return m_search_results.size(); + return mFindMatches.size(); } // normalize Whitespace: multiple whitespaces to one std::string container_qt::normalizeWhitespace( const std::string& text ) { - std::string result; + std::string normalized; bool lastWasSpace = false; for ( char c : text ) { if ( std::isspace( static_cast( c ) ) ) { - if ( !lastWasSpace && !result.empty() ) + if ( !lastWasSpace && !normalized.empty() ) { - result += ' '; + normalized += ' '; lastWasSpace = true; } } else { - result += c; + normalized += c; lastWasSpace = false; } } - return result; + return normalized; } -void container_qt::collectTextFragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText ) +void container_qt::collect_text_fragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText ) { if ( !el ) return; @@ -1188,15 +1156,15 @@ void container_qt::collectTextFragments( litehtml::element::ptr el, std::vector< for ( auto it = el->children().begin(); it != el->children().end(); ++it ) { - collectTextFragments( ( *it ), fragments, fullText ); + collect_text_fragments( ( *it ), fragments, fullText ); } } // calculate bounding box for all elements found in search -litehtml::position container_qt::calculatePreciseBoundingBox( const std::vector& allFragments, - int searchStart, - int searchEnd, - std::vector& matchedFragments ) +litehtml::position container_qt::calculate_precise_bounding_box( const std::vector& allFragments, + int searchStart, + int searchEnd, + std::vector& matchedFragments ) { litehtml::position boundingBox = { 0, 0, 0, 0 }; bool firstFragment = true; @@ -1270,10 +1238,10 @@ litehtml::position container_qt::calculatePreciseBoundingBox( const std::vector< } // search document for Text including multiword phrases -void container_qt::searchTextInDocument( litehtml::document::ptr doc, - const std::string& search_term, - std::vector& results, - bool case_sensitive ) +void container_qt::find_text_in_document( litehtml::document::ptr doc, + const std::string& search_term, + std::vector& matches, + bool case_sensitive ) { if ( !doc || search_term.empty() ) return; @@ -1281,44 +1249,44 @@ void container_qt::searchTextInDocument( litehtml::document::ptr doc, // Sammle alle Text-Fragmente mit Positionen std::vector fragments; std::string fullText; - collectTextFragments( doc->root(), fragments, fullText ); + collect_text_fragments( doc->root(), fragments, fullText ); // Normalisiere Text für Suche std::string normalizedFullText = normalizeWhitespace( fullText ); std::string normalizedSearchTerm = normalizeWhitespace( search_term ); - std::string searchText = normalizedFullText; - std::string searchFor = normalizedSearchTerm; + std::string findText = normalizedFullText; + std::string searchFor = normalizedSearchTerm; if ( !case_sensitive ) { - std::transform( searchText.begin(), searchText.end(), searchText.begin(), ::tolower ); + std::transform( findText.begin(), findText.end(), findText.begin(), ::tolower ); std::transform( searchFor.begin(), searchFor.end(), searchFor.begin(), ::tolower ); } // Alle Vorkommen finden size_t pos = 0; - while ( ( pos = searchText.find( searchFor, pos ) ) != std::string::npos ) + while ( ( pos = findText.find( searchFor, pos ) ) != std::string::npos ) { - TextSearchResult result; - result.matched_text = normalizedFullText.substr( pos, searchFor.length() ); + TextFindMatch match; + match.matched_text = normalizedFullText.substr( pos, searchFor.length() ); int searchStart = static_cast( pos ); int searchEnd = static_cast( pos + searchFor.length() ); // Berechne präzise Bounding Box - result.bounding_box = calculatePreciseBoundingBox( fragments, searchStart, searchEnd, result.fragments ); + match.bounding_box = calculate_precise_bounding_box( fragments, searchStart, searchEnd, match.fragments ); - results.push_back( result ); + matches.push_back( match ); pos += searchFor.length(); } } // Textsuche durchführen -int container_qt::search_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive ) +int container_qt::find_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive ) { - m_search_results.clear(); - m_current_result_index = -1; + mFindMatches.clear(); + mFindCurrentMatchIndex = -1; if ( !doc || search_term.empty() ) { @@ -1326,64 +1294,63 @@ int container_qt::search_text( litehtml::document::ptr doc, const std::string& s } // Suche vom Root-Element starten - // search_text_in_element( doc->root(), search_term, m_search_results, case_sensitive ); - searchTextInDocument( mDocument, search_term, m_search_results, case_sensitive ); + find_text_in_document( mDocument, search_term, mFindMatches, case_sensitive ); - if ( !m_search_results.empty() ) + if ( !mFindMatches.empty() ) { - m_current_result_index = 0; + mFindCurrentMatchIndex = 0; } - return static_cast( m_search_results.size() ); + return static_cast( mFindMatches.size() ); } // Zum nächsten Suchergebnis springen -bool container_qt::next_search_result() +bool container_qt::find_next_match() { - if ( m_search_results.empty() ) + if ( mFindMatches.empty() ) return false; - m_current_result_index++; - if ( m_current_result_index >= static_cast( m_search_results.size() ) ) + mFindCurrentMatchIndex++; + if ( mFindCurrentMatchIndex >= static_cast( mFindMatches.size() ) ) { - m_current_result_index = 0; // Wrap-around + mFindCurrentMatchIndex = 0; // Wrap-around } return true; } // Zum vorherigen Suchergebnis springen -bool container_qt::previous_search_result() +bool container_qt::find_previous_match() { - if ( m_search_results.empty() ) + if ( mFindMatches.empty() ) return false; - m_current_result_index--; - if ( m_current_result_index < 0 ) + mFindCurrentMatchIndex--; + if ( mFindCurrentMatchIndex < 0 ) { - m_current_result_index = static_cast( m_search_results.size() ) - 1; // Wrap-around + mFindCurrentMatchIndex = static_cast( mFindMatches.size() ) - 1; // Wrap-around } return true; } // Aktuelles Suchergebnis mit Position holen -const container_qt::TextSearchResult* container_qt::get_current_result() const +const container_qt::TextFindMatch* container_qt::find_current_match() const { - if ( m_current_result_index >= 0 && m_current_result_index < static_cast( m_search_results.size() ) ) + if ( mFindCurrentMatchIndex >= 0 && mFindCurrentMatchIndex < static_cast( mFindMatches.size() ) ) { - return &m_search_results[m_current_result_index]; + return &mFindMatches[mFindCurrentMatchIndex]; } return nullptr; } void container_qt::draw_highlights( litehtml::uint_ptr hdc ) { - for ( auto it = m_search_results.begin(); it != m_search_results.end(); ++it ) + for ( auto it = mFindMatches.begin(); it != mFindMatches.end(); ++it ) { - const auto result = ( *it ); - for ( auto it = result.fragments.begin(); it != result.fragments.end(); ++it ) + const auto match = ( *it ); + for ( auto it = match.fragments.begin(); it != match.fragments.end(); ++it ) { litehtml::position pos = ( *it ).pos; - highlight_text_at_position( hdc, pos, result.matched_text ); + highlight_text_at_position( hdc, pos, match.matched_text ); } } } @@ -1409,30 +1376,30 @@ void container_qt::highlight_text_at_position( litehtml::uint_ptr hdc, const lit p->restore(); } -void container_qt::scrollToNextSearchResult() +void container_qt::findNextMatch() { - if ( next_search_result() ) + if ( find_next_match() ) { - const auto* result = get_current_result(); - scrollToSearchResult( result ); + const auto* match = find_current_match(); + scroll_to_find_match( match ); } } -void container_qt::scrollToPreviousSearchResult() +void container_qt::findPreviousMatch() { - if ( previous_search_result() ) + if ( find_previous_match() ) { - const auto* result = get_current_result(); - scrollToSearchResult( result ); + const auto* match = find_current_match(); + scroll_to_find_match( match ); } } -void container_qt::scrollToSearchResult( const TextSearchResult* result ) +void container_qt::scroll_to_find_match( const TextFindMatch* match ) { - if ( !result ) + if ( !match ) { return; } - horizontalScrollBar()->setValue( result->bounding_box.left() ); - verticalScrollBar()->setValue( result->bounding_box.top() ); + horizontalScrollBar()->setValue( match->bounding_box.left() ); + verticalScrollBar()->setValue( match->bounding_box.top() ); } diff --git a/src/container_qt.h b/src/container_qt.h index 0f3f19f..b2361a0 100644 --- a/src/container_qt.h +++ b/src/container_qt.h @@ -31,9 +31,9 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co void setOpenExternalLinks( bool open ) { mOpenExternLinks = open; } const QString& caption() const { return mCaption; } void print( QPagedPaintDevice* paintDevice ); - int searchText( const QString& text ); - void scrollToNextSearchResult(); - void scrollToPreviousSearchResult(); + int findText( const QString& text ); + void findNextMatch(); + void findPreviousMatch(); void setHighlightColor( const QColor& color ) { mHighlightColor = color; } QColor highlightColor() const { return mHighlightColor; } @@ -121,31 +121,31 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co int end_offset; // end Offset in element text }; - struct TextSearchResult + struct TextFindMatch { std::string matched_text; std::vector fragments; // may contains multiple elements litehtml::position bounding_box; // bounding box over all elements }; - int search_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive = true ); - bool next_search_result(); - bool previous_search_result(); - const TextSearchResult* get_current_result() const; - void highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const std::string& text ); - void draw_highlights( litehtml::uint_ptr hdc ); - void clear_highlights() { m_search_results.clear(); } - std::string normalizeWhitespace( const std::string& text ); - void searchTextInDocument( litehtml::document::ptr doc, - const std::string& search_term, - std::vector& results, - bool case_sensitive = true ); - void collectTextFragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText ); - litehtml::position calculatePreciseBoundingBox( const std::vector& allFragments, + int find_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive = true ); + bool find_next_match(); + bool find_previous_match(); + const TextFindMatch* find_current_match() const; + void highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const std::string& text ); + void draw_highlights( litehtml::uint_ptr hdc ); + void clear_highlights() { mFindMatches.clear(); } + std::string normalizeWhitespace( const std::string& text ); + void find_text_in_document( litehtml::document::ptr doc, + const std::string& search_term, + std::vector& matches, + bool case_sensitive = true ); + void collect_text_fragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText ); + litehtml::position calculate_precise_bounding_box( const std::vector& allFragments, int searchStart, int searchEnd, std::vector& matchedFragments ); - void scrollToSearchResult( const TextSearchResult* ); + void scroll_to_find_match( const TextFindMatch* ); std::shared_ptr mDocument; QByteArray mDocumentSource; @@ -166,8 +166,8 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co QString mUserCSS; QStack mClipStack; litehtml::position mClip = {}; - std::vector m_search_results = {}; - int m_current_result_index = -1; + std::vector mFindMatches = {}; + int mFindCurrentMatchIndex = -1; QColor mHighlightColor = QColor( 255, 255, 0, 30 ); - QString mSearchTerm; + QString mFindText; }; diff --git a/test/browser/testbrowser.cpp b/test/browser/testbrowser.cpp index 504ea81..96bb21d 100644 --- a/test/browser/testbrowser.cpp +++ b/test/browser/testbrowser.cpp @@ -68,28 +68,28 @@ TestBrowser::TestBrowser() } } ); - mSearchText = new QLineEdit( this ); - mToolBar.addWidget( mSearchText ); - connect( mSearchText, &QLineEdit::returnPressed, this, + mFindText = new QLineEdit( this ); + mToolBar.addWidget( mFindText ); + connect( mFindText, &QLineEdit::returnPressed, this, [this]() { - if ( mSearchText != nullptr ) + if ( mFindText != nullptr ) { - searchText( mSearchText->text() ); + findText( mFindText->text() ); } } ); // Vorheriges mit Standard-Icon (Pfeil nach oben) - mPreviousSearchResult = new QAction( style()->standardIcon( QStyle::SP_ArrowUp ), "Vorheriges", this ); - connect( mPreviousSearchResult, &QAction::triggered, this, [this]() { previousSearchResult(); } ); - mPreviousSearchResult->setEnabled( false ); - mToolBar.addAction( mPreviousSearchResult ); + mPreviousFindMatch = new QAction( style()->standardIcon( QStyle::SP_ArrowUp ), "Vorheriges", this ); + connect( mPreviousFindMatch, &QAction::triggered, this, [this]() { previousFindMatch(); } ); + mPreviousFindMatch->setEnabled( false ); + mToolBar.addAction( mPreviousFindMatch ); - mNextSearchResult = new QAction( style()->standardIcon( QStyle::SP_ArrowDown ), "Nächstes", this ); - mNextSearchResult->setEnabled( false ); - connect( mNextSearchResult, &QAction::triggered, this, [this]() { nextSearchResult(); } ); + mNextFindMatch = new QAction( style()->standardIcon( QStyle::SP_ArrowDown ), "Nächstes", this ); + mNextFindMatch->setEnabled( false ); + connect( mNextFindMatch, &QAction::triggered, this, [this]() { nextFindMatch(); } ); - mToolBar.addAction( mNextSearchResult ); + mToolBar.addAction( mNextFindMatch ); setMenuBar( &mMenu ); addToolBar( Qt::TopToolBarArea, &mToolBar ); @@ -204,19 +204,19 @@ void TestBrowser::export2pdf() } #endif } -void TestBrowser::searchText( const QString& text ) +void TestBrowser::findText( const QString& text ) { - auto found = mBrowser->searchText( text ); - mPreviousSearchResult->setEnabled( found > 0 ); - mNextSearchResult->setEnabled( found > 0 ); + auto found = mBrowser->findText( text ); + mPreviousFindMatch->setEnabled( found > 0 ); + mNextFindMatch->setEnabled( found > 0 ); } -void TestBrowser::previousSearchResult() +void TestBrowser::previousFindMatch() { - mBrowser->scrollToPreviousSearchResult(); + mBrowser->findPreviousMatch(); } -void TestBrowser::nextSearchResult() +void TestBrowser::nextFindMatch() { - mBrowser->scrollToNextSearchResult(); + mBrowser->findNextMatch(); } diff --git a/test/browser/testbrowser.h b/test/browser/testbrowser.h index c9bfb74..0c1953a 100644 --- a/test/browser/testbrowser.h +++ b/test/browser/testbrowser.h @@ -35,18 +35,18 @@ class TestBrowser : public QMainWindow void openHelp(); void loadHelp(); void export2pdf(); - void searchText( const QString& text ); - void nextSearchResult(); - void previousSearchResult(); + void findText( const QString& text ); + void nextFindMatch(); + void previousFindMatch(); private: QHelpBrowser* mBrowser; QMenuBar mMenu; QDir mLastDirectory; - QLineEdit* mUrl = nullptr; - QLineEdit* mSearchText = nullptr; + QLineEdit* mUrl = nullptr; + QLineEdit* mFindText = nullptr; QToolBar mToolBar; - QHelpEngine* mHelpEngine = nullptr; - QAction* mNextSearchResult = nullptr; - QAction* mPreviousSearchResult = nullptr; + QHelpEngine* mHelpEngine = nullptr; + QAction* mNextFindMatch = nullptr; + QAction* mPreviousFindMatch = nullptr; }; diff --git a/test/library/CMakeLists.txt b/test/library/CMakeLists.txt index 2572496..2ca2bfe 100644 --- a/test/library/CMakeLists.txt +++ b/test/library/CMakeLists.txt @@ -12,7 +12,7 @@ set( test_names test_html_content test_css_content test_history - test_search + test_find ) set( runpath "$;$") diff --git a/test/library/test_search.cpp b/test/library/test_find.cpp similarity index 74% rename from test/library/test_search.cpp rename to test/library/test_find.cpp index 28333c0..7fb528a 100644 --- a/test/library/test_search.cpp +++ b/test/library/test_find.cpp @@ -1,4 +1,4 @@ -#include "test_search.h" +#include "test_find.h" #include #include @@ -8,11 +8,11 @@ int main( int argc, char** argv ) { QApplication app( argc, argv ); - SearchTest mContentTest; + FindTest mContentTest; return QTest::qExec( &mContentTest, mContentTest.args() ); } -QLiteHtmlBrowser* SearchTest::createMainWindow( const QSize& size ) +QLiteHtmlBrowser* FindTest::createMainWindow( const QSize& size ) { mWnd = std::make_unique(); auto browser = new QLiteHtmlBrowser( nullptr ); @@ -24,7 +24,7 @@ QLiteHtmlBrowser* SearchTest::createMainWindow( const QSize& size ) return browser; } -void SearchTest::test_search_single_word() +void FindTest::test_find_single_word() { auto browser = createMainWindow( mBrowserSize ); // QVERIFY( mWnd->centralWidget() ); @@ -34,20 +34,20 @@ void SearchTest::test_search_single_word() auto base = QString{ TEST_SOURCE_DIR } + "/search/"; browser->setSource( QUrl::fromLocalFile( base + "/LongGermanText.html" ) ); - auto found = browser->searchText( ( "Sätze" ) ); + auto found = browser->findText( ( "Sätze" ) ); qApp->processEvents(); QCOMPARE( found, 40 ); - found = browser->searchText( ( " Er" ) ); + found = browser->findText( ( " Er" ) ); qApp->processEvents(); QCOMPARE( found, 46 ); - found = browser->searchText( "" ); + found = browser->findText( "" ); qApp->processEvents(); QCOMPARE( found, 0 ); } -void SearchTest::test_search_phrase() +void FindTest::test_find_phrase() { auto browser = createMainWindow( mBrowserSize ); // QVERIFY( mWnd->centralWidget() ); @@ -57,16 +57,16 @@ void SearchTest::test_search_phrase() auto base = QString{ TEST_SOURCE_DIR } + "/search/"; browser->setSource( QUrl::fromLocalFile( base + "/LongGermanText.html" ) ); - auto found = browser->searchText( "allgemeine Beobachtungen" ); + auto found = browser->findText( "allgemeine Beobachtungen" ); qApp->processEvents(); QCOMPARE( found, 1 ); - found = browser->searchText( "schliesst ab" ); + found = browser->findText( "schliesst ab" ); qApp->processEvents(); QCOMPARE( found, 0 ); } -void SearchTest::test_search_phrase_multi_element() +void FindTest::test_find_phrase_multi_element() { auto browser = createMainWindow( mBrowserSize ); // QVERIFY( mWnd->centralWidget() ); @@ -74,7 +74,7 @@ void SearchTest::test_search_phrase_multi_element() auto base = QString{ TEST_SOURCE_DIR } + "/search/"; browser->setSource( QUrl::fromLocalFile( base + "/LongGermanText.html" ) ); - auto found = browser->searchText( QStringLiteral( "schließt ab. Absatz 48" ) ); + auto found = browser->findText( QStringLiteral( "schließt ab. Absatz 48" ) ); qApp->processEvents(); QCOMPARE( found, 1 ); } diff --git a/test/library/test_search.h b/test/library/test_find.h similarity index 72% rename from test/library/test_search.h rename to test/library/test_find.h index 01613d2..c88c610 100644 --- a/test/library/test_search.h +++ b/test/library/test_find.h @@ -5,13 +5,13 @@ class QLiteHtmlBrowser; -class SearchTest : public TestBase +class FindTest : public TestBase { Q_OBJECT public: - SearchTest() + FindTest() : TestBase( qApp->arguments() ){}; - virtual ~SearchTest() = default; + virtual ~FindTest() = default; private Q_SLOTS: void init() { TestBase::init(); } @@ -21,9 +21,9 @@ private Q_SLOTS: mWnd.reset(); mWnd = nullptr; } - void test_search_single_word(); - void test_search_phrase(); - void test_search_phrase_multi_element(); + void test_find_single_word(); + void test_find_phrase(); + void test_find_phrase_multi_element(); private: QLiteHtmlBrowser* createMainWindow( const QSize& ); From a3743c66887dc7cbc1de25962020d753de578b6f Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Thu, 23 Oct 2025 13:16:50 +0200 Subject: [PATCH 11/11] add comments for some interface functions --- include/qlitehtmlbrowser/QLiteHtmlBrowser.h | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h index 9abd409..f2124e3 100644 --- a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h +++ b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h @@ -127,11 +127,16 @@ class QLITEHTMLBROWSER_EXPORT QLiteHtmlBrowser : public QWidget /// set file system paths where relative urls or resources are resolved void setSearchPaths( const QStringList& ); + /// return true if history stack can go backward bool isBackwardAvailable() const; + /// return true if history stack go go forward bool isForwardAvailable() const; + /// clear the history void clearHistory(); - int backwardHistoryCount() const; - int forwardHistoryCount() const; + /// number of pages in history stack backwards + int backwardHistoryCount() const; + /// number of pages in history stack forwards + int forwardHistoryCount() const; /// return title for url if available, else returns empty string const QString& caption() const; @@ -140,8 +145,10 @@ class QLITEHTMLBROWSER_EXPORT QLiteHtmlBrowser : public QWidget void print( QPagedPaintDevice* printer ) const; /// find text in document - int findText( const QString& ); + int findText( const QString& ); + /// move to next match void findNextMatch(); + /// move to previous match void findPreviousMatch(); /// configure interface for highlight color, i.e. to highlight found text @@ -157,7 +164,9 @@ public Q_SLOTS: virtual void setSource( const QUrl& name ); void setSource( const QUrl& url, ResourceType type ); + /// go backward in history stack virtual void backward(); + /// go forward in history stack virtual void forward(); /// home url is the first url set via setSource for this instance. @@ -169,9 +178,6 @@ public Q_SLOTS: protected: Q_SIGNALS: - // void backwardAvailable(bool); - // void forwardAvailable(bool); - // void historyChanged(); /// emitted when the url changed due to user interaction, e.g. link activation, @see setOpenLinks void sourceChanged( const QUrl& ); @@ -179,9 +185,6 @@ public Q_SLOTS: /// send when an anchor is clicked in the document, even if link is not activated, @see setOpenLinks void anchorClicked( const QUrl& ); - // selection, copy, paste etc currently not implemented - // void highlighted(const QUrl &); - private: QLiteHtmlBrowserImpl* mImpl = nullptr; };