diff --git a/bridges/KinoboxBridge.php b/bridges/KinoboxBridge.php
new file mode 100644
index 00000000000..1c84b26d14d
--- /dev/null
+++ b/bridges/KinoboxBridge.php
@@ -0,0 +1,310 @@
+ [
+ ],
+ ];
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ defaultLinkTo($html, static::URI);
+
+ // Router
+ switch ($this->queriedContext) {
+ case 'Articles':
+ $this->collectNews($html);
+ break;
+ }
+ }
+
+ /**
+ * Returns the icon for the bridge.
+ *
+ * @return string The icon URL.
+ */
+ public function getURI()
+ {
+ $uri = static::URI;
+
+ // URI Router
+ switch ($this->queriedContext) {
+ case 'Articles':
+ $uri .= '/clanky';
+ break;
+ }
+
+ return $uri;
+ }
+
+ /**
+ * Returns the name for the bridge.
+ *
+ * @return string The Name.
+ */
+ public function getName()
+ {
+ $name = static::NAME;
+
+ $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : '';
+
+ switch ($this->queriedContext) {
+ case 'Articles':
+ break;
+ }
+
+ return $name;
+ }
+
+ /**
+ * Parse most used date formats
+ *
+ * Basically strtotime doesn't convert dates correctly due to formats
+ * being hard to interpret. So we use the DateTime object, manually
+ * fixing dates and times (set to 00:00:00.000).
+ *
+ * We don't know the timezone, so just assume +00:00 (or whatever
+ * DateTime chooses)
+ */
+ private function fixDate($date)
+ {
+ // Define the regular expression pattern.
+ // It matches any character that is NOT (^):
+ // - a digit (0-9)
+ // - a forward slash (/)
+ // - a dot (.)
+ // - a colon (:)
+ // - a hyphen (-)
+ // - a whitespace character (\s includes space, tab, newline etc.)
+ // The 'u' modifier ensures it works correctly with UTF-8 strings.
+ $pattern = '/[^0-9\/\.\:\-]/u';
+
+ // Use preg_replace to remove all characters matching the pattern (i.e., unwanted chars).
+ // Replace them with an empty string ''.
+ $date = preg_replace($pattern, '', $date);
+
+ // parse date based on most used formats
+ $df = $this->parseDateTimeFromString($date);
+
+ return date_format($df, 'U');
+ }
+
+ /**
+ * Extracts the images from the article.
+ *
+ * @param object $article The article object.
+ * @return array An array of image URLs.
+ */
+ private function extractNewsImages($article)
+ {
+ // Notice: We can have zero or more images (though it should mostly be 1)
+ $srcset = $article->find('img', 0)->getAttribute('srcset');
+
+ $images = [];
+
+ // split multi srcset into array of images
+ $img_parts = preg_split('/\s+/', $srcset);
+ // Only 3th array is interesting
+ $images[] = html_entity_decode($img_parts[2]);
+
+ return $images;
+ }
+
+ #region Articles
+
+ /**
+ * Collects uri, timestamp, title, content and images in the news articles from the HTML and transforms to rss.
+ *
+ * @param object $html The HTML object.
+ * @return void
+ */
+ private function collectNews($html)
+ {
+ // Check if page contains articles
+ $articles = $html->find('ol.ArticlesPagingListContainer_grid__OzyvS', 0)->find('li')
+ or returnServerException('No articles found! Layout might have changed!');
+
+ foreach ($articles as $article) {
+ $item = [];
+
+ // get uri of product
+ $item['uri'] = $this->extractNewsUri($article);
+ // Add images
+ $item['enclosures'] = $this->extractNewsImages($article);
+ // Add title
+ $item['title'] = $this->extractNewsTitle($article);
+ // Add description
+ $item['timestamp'] = $this->extractNewsDate($article);
+ // Add content
+ $item['content'] = $this->extractNewsDescription($article);
+
+ // collect sources into rss article
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Extracts the URI of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The URI of the news article.
+ */
+ private function extractNewsUri($article)
+ {
+ // Return URI of the article
+ $element = $article->find('a', 0)
+ or returnServerException('Anchor not found!');
+
+ return $element->href;
+ }
+
+ /**
+ * Extracts the date of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The date of the news article.
+ */
+ private function extractNewsDate($article)
+ {
+ // Check if date is set
+ $element = $article->find('.Space_container__MY8CT', 0)
+ or returnServerException('Date not found!');
+
+ // Format date
+ return $this->fixDate($element->plaintext);
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription($article)
+ {
+ // Extract description
+ $element = $article->find('strong', 0)
+ or returnServerException('Description not found!');
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the title of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The title of the news article.
+ */
+ private function extractNewsTitle($article)
+ {
+ // Extract title
+ $element = $article->find('strong', 0)
+ or returnServerException('Title not found!');
+
+ return $element->plaintext;
+ }
+
+ /**
+ * It attempts to recognize the date/time format in a string and create a DateTime object.
+ *
+ * It goes through the list of defined formats and tries to apply them to the input string.
+ * Returns the first successfully parsed DateTime object that matches the entire string.
+ *
+ * @param string $dateString A string potentially containing a date and/or time.
+ * @return DateTime|null A DateTime object if successfully recognized and parsed, otherwise null.
+ */
+ private function parseDateTimeFromString(string $dateString): ?DateTime
+ {
+ // List of common formats - YOU CAN AND SHOULD EXPAND IT according to expected inputs!
+ // Order may matter if the formats are ambiguous.
+ // It is recommended to give more specific formats (with time, full year) before more general ones.
+ $possibleFormats = [
+ // Czech formats (day.month.year)
+ 'd.m.Y H:i:s', // 10.04.2025 10:57:47
+ 'j.n.Y H:i:s', // 10.4.2025 10:57:47
+ 'd. m. Y H:i:s', // 10. 04. 2025 10:57:47
+ 'j. n. Y H:i:s', // 10. 4. 2025 10:57:47
+ 'd.m.Y H:i', // 10.04.2025 10:57
+ 'j.n.Y H:i', // 10.4.2025 10:57
+ 'd. m. Y H:i', // 10. 04. 2025 10:57
+ 'j. n. Y H:i', // 10. 4. 2025 10:57
+ 'd.m.Y', // 10.04.2025
+ 'j.n.Y', // 10.4.2025
+ 'd. m. Y', // 10. 04. 2025
+ 'j. n. Y', // 10. 4. 2025
+
+ // ISO 8601 and international formats (year-month-day)
+ 'Y-m-d H:i:s', // 2025-04-10 10:57:47
+ 'Y-m-d H:i', // 2025-04-10 10:57
+ 'Y-m-d', // 2025-04-10
+ 'YmdHis', // 20250410105747
+ 'Ymd', // 20250410
+
+ // American formats (month/day/year) - beware of ambiguity!
+ 'm/d/Y H:i:s', // 04/10/2025 10:57:47
+ 'n/j/Y H:i:s', // 4/10/2025 10:57:47
+ 'm/d/Y H:i', // 04/10/2025 10:57
+ 'n/j/Y H:i', // 4/10/2025 10:57
+ 'm/d/Y', // 04/10/2025
+ 'n/j/Y', // 4/10/2025
+
+ // Standard formats (including time zone)
+ DateTime::ATOM, // example. 2025-04-10T10:57:47+02:00
+ DateTime::RFC3339, // example. 2025-04-10T10:57:47+02:00
+ DateTime::RFC3339_EXTENDED, // example. 2025-04-10T10:57:47.123+02:00
+ DateTime::RFC2822, // example. Thu, 10 Apr 2025 10:57:47 +0200
+ DateTime::ISO8601, // example. 2025-04-10T105747+0200
+ 'Y-m-d\TH:i:sP', // ISO 8601 s 'T' oddělovačem
+ 'Y-m-d\TH:i:s.uP', // ISO 8601 s mikrosekundami
+
+ // You can add more formats as needed...
+ // e.g. 'd-M-Y' (10-Apr-2025) - requires English locale
+ // e.g. 'j. F Y' (10. abren 2025) - requires Czech locale
+ ];
+
+ // Set locale for parsing month/day names (if using F, M, l, D)
+ // E.g. setlocale(LC_TIME, 'cs_CZ.UTF-8'); or 'en_US.UTF-8');
+
+ foreach ($possibleFormats as $format) {
+ // We will try to create a DateTime object from the given format
+ $dateTime = DateTime::createFromFormat($format, $dateString);
+
+ // We check that the parsing was successful AND ALSO
+ // that there were no errors or warnings during the parsing.
+ // This is important to ensure that the format matches the ENTIRE string.
+ if ($dateTime !== false) {
+ $errors = DateTime::getLastErrors();
+ if (!($errors)) {
+ // Success! We found a valid format for the entire string.
+ return $dateTime;
+ }
+ }
+ }
+
+ // If no format matches or parsing failed
+ return null;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/bridges/MakroBridge.php b/bridges/MakroBridge.php
new file mode 100644
index 00000000000..0930d859072
--- /dev/null
+++ b/bridges/MakroBridge.php
@@ -0,0 +1,339 @@
+ [],
+ 'TOP nabídky' => []
+ ];
+
+ /**
+ * Fetches and processes data based on the selected context.
+ *
+ * This function retrieves the HTML content for the specified context's URI,
+ * resolves relative links within the content, and then delegates the data
+ * extraction to the appropriate method (currently only `collectNews`).
+ */
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ defaultLinkTo($html, static::URI);
+
+ // Router
+ switch ($this->queriedContext) {
+ case 'Aktuální letáky':
+ $this->collectNews($html);
+ break;
+ case 'TOP nabídky':
+ $this->collectLetter($html);
+ break;
+ }
+ }
+
+ /**
+ * Returns the icon for the bridge.
+ *
+ * @return string The icon URL.
+ */
+ public function getURI()
+ {
+ $uri = static::URI;
+
+ // URI Router
+ switch ($this->queriedContext) {
+ case 'Aktuální letáky':
+ $uri .= '/aktualni-nabidka';
+ break;
+ case 'TOP nabídky':
+ $uri .= '/top-nabidky';
+ break;
+ }
+
+ return $uri;
+ }
+
+ /**
+ * Returns the name for the bridge.
+ *
+ * @return string The Name.
+ */
+ public function getName()
+ {
+ $name = static::NAME;
+
+ $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : '';
+
+ switch ($this->queriedContext) {
+ case 'Aktuální letáky':
+ break;
+ case 'TOP nabídky':
+ break;
+ }
+
+ return $name;
+ }
+
+ /**
+ * Parse most used date formats
+ *
+ * Basically strtotime doesn't convert dates correctly due to formats
+ * being hard to interpret. So we use the DateTime object, manually
+ * fixing dates and times (set to 00:00:00.000).
+ *
+ * We don't know the timezone, so just assume +00:00 (or whatever
+ * DateTime chooses)
+ */
+ private function fixDate($date)
+ {
+ $df = $this->parseDateTimeFromString($date);
+
+ return date_format($df, 'U');
+ }
+
+ /**
+ * Extracts the images from the article.
+ *
+ * @param object $article The article object.
+ * @return array An array of image URLs.
+ */
+ private function extractImages($article)
+ {
+ // Notice: We can have zero or more images (though it should mostly be 1)
+ $elements = $article->find('img');
+
+ $images = [];
+
+ foreach ($elements as $img) {
+ $images[] = $img->src;
+ }
+
+ return $images;
+ }
+
+ #region Articles
+
+ /**
+ * Collects uri, timestamp, title, content and images in the news articles from the HTML and transforms to rss.
+ *
+ * @param object $html The HTML object.
+ * @return void
+ */
+ private function collectNews($html)
+ {
+ // Check if page contains articles
+ $articles = $html->find('.catalog')
+ or returnServerException('No articles found! Layout might have changed!');
+
+ foreach ($articles as $article) {
+ $item = [];
+
+ // Extract article URI
+ $item['uri'] = $this->extractNewsUri($article);
+
+ // Extract article title
+ $item['title'] = $this->extractNewsTitle($article);
+
+ // Extract article content
+ $item['content'] = $this->extractNewsDescription($article);
+
+ // Extract article images
+ $item['enclosures'] = $this->extractImages($article);
+
+ // collect sources into rss article
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Collects uri, timestamp, title, content and images in the letter articles from the HTML and transforms to rss.
+ *
+ * @param object $html The HTML object.
+ * @return void
+ */
+ private function collectLetter($html)
+ {
+ // Check if page contains articles
+ $articles = $html->find('.catalog')
+ or returnServerException('No articles found! Layout might have changed!');
+
+ foreach ($articles as $article) {
+ $item = [];
+
+ // Extract article URI
+ $item['uri'] = $this->extractNewsUri($article);
+
+ // Extract article title
+ $item['title'] = $this->extractNewsTitle($article);
+
+ // Extract article content
+ $item['content'] = $this->extractNewsDescription($article);
+
+ // Extract article images
+ $item['enclosures'] = $this->extractImages($article);
+
+ // collect sources into rss article
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Extracts the URI of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The URI of the news article.
+ */
+ private function extractNewsUri($article)
+ {
+ // Return URI of the article
+ $element = $article->find('a', 0)
+ or returnServerException('Anchor not found!');
+
+ return $element->href;
+ }
+
+ /**
+ * Extracts the date of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The date of the news article.
+ */
+ private function extractNewsDate($article)
+ {
+ // Check if date is set
+ $element = $article->find('div.post__info', 0)->find('span', 0)
+ or returnServerException('Date not found!');
+
+ $date = trim(explode('|', $element->plaintext)[0]);
+
+ // Format date
+ return $this->fixDate($date);
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription($article)
+ {
+ // Extract description
+ $element = $article->find('div.catalog-content', 0)->find('span', 1)
+ or returnServerException('Title not found!');
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the title of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The title of the news article.
+ */
+ private function extractNewsTitle($article)
+ {
+ // Extract title
+ $element = $article->find('span.catalog-title', 0)->find('strong', 0)
+ or returnServerException('Title not found!');
+
+ return $element->plaintext;
+ }
+
+ /**
+ * It attempts to recognize the date/time format in a string and create a DateTime object.
+ *
+ * It goes through the list of defined formats and tries to apply them to the input string.
+ * Returns the first successfully parsed DateTime object that matches the entire string.
+ *
+ * @param string $dateString A string potentially containing a date and/or time.
+ * @return DateTime|null A DateTime object if successfully recognized and parsed, otherwise null.
+ */
+ private function parseDateTimeFromString(string $dateString): ?DateTime
+ {
+ // List of common formats - YOU CAN AND SHOULD EXPAND IT according to expected inputs!
+ // Order may matter if the formats are ambiguous.
+ // It is recommended to give more specific formats (with time, full year) before more general ones.
+ $possibleFormats = [
+ // Czech formats (day.month.year)
+ 'd.m.Y H:i:s', // 10.04.2025 10:57:47
+ 'j.n.Y H:i:s', // 10.4.2025 10:57:47
+ 'd. m. Y H:i:s', // 10. 04. 2025 10:57:47
+ 'j. n. Y H:i:s', // 10. 4. 2025 10:57:47
+ 'd.m.Y H:i', // 10.04.2025 10:57
+ 'j.n.Y H:i', // 10.4.2025 10:57
+ 'd. m. Y H:i', // 10. 04. 2025 10:57
+ 'j. n. Y H:i', // 10. 4. 2025 10:57
+ 'd.m.Y', // 10.04.2025
+ 'j.n.Y', // 10.4.2025
+ 'd. m. Y', // 10. 04. 2025
+ 'j. n. Y', // 10. 4. 2025
+
+ // ISO 8601 and international formats (year-month-day)
+ 'Y-m-d H:i:s', // 2025-04-10 10:57:47
+ 'Y-m-d H:i', // 2025-04-10 10:57
+ 'Y-m-d', // 2025-04-10
+ 'YmdHis', // 20250410105747
+ 'Ymd', // 20250410
+
+ // American formats (month/day/year) - beware of ambiguity!
+ 'm/d/Y H:i:s', // 04/10/2025 10:57:47
+ 'n/j/Y H:i:s', // 4/10/2025 10:57:47
+ 'm/d/Y H:i', // 04/10/2025 10:57
+ 'n/j/Y H:i', // 4/10/2025 10:57
+ 'm/d/Y', // 04/10/2025
+ 'n/j/Y', // 4/10/2025
+
+ // Standard formats (including time zone)
+ DateTime::ATOM, // example. 2025-04-10T10:57:47+02:00
+ DateTime::RFC3339, // example. 2025-04-10T10:57:47+02:00
+ DateTime::RFC3339_EXTENDED, // example. 2025-04-10T10:57:47.123+02:00
+ DateTime::RFC2822, // example. Thu, 10 Apr 2025 10:57:47 +0200
+ DateTime::ISO8601, // example. 2025-04-10T105747+0200
+ 'Y-m-d\TH:i:sP', // ISO 8601 s 'T' oddělovačem
+ 'Y-m-d\TH:i:s.uP', // ISO 8601 s mikrosekundami
+
+ // You can add more formats as needed...
+ // e.g. 'd-M-Y' (10-Apr-2025) - requires English locale
+ // e.g. 'j. F Y' (10. abren 2025) - requires Czech locale
+ ];
+
+ // Set locale for parsing month/day names (if using F, M, l, D)
+ // E.g. setlocale(LC_TIME, 'cs_CZ.UTF-8'); or 'en_US.UTF-8');
+
+ foreach ($possibleFormats as $format) {
+ // We will try to create a DateTime object from the given format
+ $dateTime = DateTime::createFromFormat($format, $dateString);
+
+ // We check that the parsing was successful AND ALSO
+ // that there were no errors or warnings during the parsing.
+ // This is important to ensure that the format matches the ENTIRE string.
+ if ($dateTime !== false) {
+ $errors = DateTime::getLastErrors();
+ if (!($errors)) {
+ // Success! We found a valid format for the entire string.
+ return $dateTime;
+ }
+ }
+ }
+
+ // If no format matches or parsing failed
+ return null;
+ }
+
+ #endregion
+}
diff --git a/bridges/PennyBridge.php b/bridges/PennyBridge.php
new file mode 100644
index 00000000000..9eeecc7ad8f
--- /dev/null
+++ b/bridges/PennyBridge.php
@@ -0,0 +1,699 @@
+ [],
+ 'Promotional letter' => []
+ ];
+
+ // Due to the fact that the URI is not static on Penny Market website (generated by javascript), we need to set it here
+ // Define your associative array (map) of keywords and URLs. This is mapping between penny product title and image URL.
+ // Keys should ideally be in lowercase for reliable comparison.
+ // Keys only without diacritics
+
+ /**
+ * Fetches and processes data based on the selected context.
+ *
+ * This function retrieves the HTML content for the specified context's URI,
+ * resolves relative links within the content, and then delegates the data
+ * extraction to the appropriate method (currently only `collectNews`).
+ */
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOMCached($this->getURI(), self::CACHE_TIMEOUT);
+
+ defaultLinkTo($html, static::URI);
+
+ // Router
+ switch ($this->queriedContext) {
+ case 'Weekly offer':
+ $this->collectNews($html);
+ break;
+ case 'Promotional letter':
+ $this->collectLetter($html); // aaaaa
+ break;
+ }
+ }
+
+ /**
+ * Returns the icon for the bridge.
+ *
+ * @return string The icon URL.
+ */
+ public function getURI()
+ {
+ $uri = static::URI;
+
+ // URI Router
+ switch ($this->queriedContext) {
+ case 'Weekly offer':
+ $uri .= 'nabidky';
+ break;
+ case 'Promotional letter':
+ $uri .= 'nabidky/letaky';
+ break;
+ }
+
+ return $uri;
+ }
+
+ /**
+ * Returns the name for the bridge.
+ *
+ * @return string The Name.
+ */
+ public function getName()
+ {
+ $name = static::NAME;
+
+ $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : '';
+
+ switch ($this->queriedContext) {
+ case 'Weekly offer':
+ break;
+ case 'Promotional letter':
+ break;
+ }
+
+ return $name;
+ }
+
+ /**
+ * Parse most used date formats
+ *
+ * Basically strtotime doesn't convert dates correctly due to formats
+ * being hard to interpret. So we use the DateTime object, manually
+ * fixing dates and times (set to 00:00:00.000).
+ *
+ * We don't know the timezone, so just assume +00:00 (or whatever
+ * DateTime chooses)
+ */
+ private function fixDate($date)
+ {
+ $df = $this->parseDateTimeFromString($date);
+
+ return date_format($df, 'U');
+ }
+
+ /**
+ * Extracts the images from the article.
+ *
+ * @param object $article The article object.
+ * @return array An array of image URLs.
+ */
+ private function extractImages($article)
+ {
+ // Notice: We can have zero or more images (though it should mostly be 1)
+ $elements = $article->find('img');
+
+ $images = [];
+
+ foreach ($elements as $img) {
+ $images[] = $img->src;
+ }
+
+ return $images;
+ }
+
+ // region Weekly offer
+
+ /**
+ * Collects uri, timestamp, title, content and images in the product offers from the HTML and transforms to rss.
+ *
+ * @param object $html The HTML object.
+ * @return void
+ */
+ private function collectNews($html)
+ {
+ $urlField = [];
+
+ // Check if page contains articles and split by class
+ $javascript = $html->find('script') or
+ returnServerException('No articles found! Layout might have changed!');
+
+ // Loop through the javascript to find the JSON data. The JSON data is in the form of a string, so we need to decode it
+ foreach ($javascript as $script) {
+ if ($script->hasAttribute('data-nuxt-data')) {
+ $json = json_decode($script->innertext);
+ }
+ }
+
+ // define the needle. This is the URL prefix we are looking for
+ $needle = 'https://images.cdn.europe';
+
+ // Loop through the JSON data to find the image URLs and extract the product codes
+ foreach ($json as $key) { // This will search in the 2 jsons
+ if (gettype($key) === 'string') {
+ if (strpos($key, $needle) === 0) {
+ $value = $this->extractProductCode($key);
+ $value = preg_replace('/-/', '', $value);
+ $urlField[$value] = $key;
+ }
+ }
+ }
+
+ // Check if page contains articles and split by class
+ $articles = $html->find('.ws-product-item-base') or
+ returnServerException('No articles found! Layout might have changed!');
+
+ // Articles loop
+ foreach ($articles as $article) {
+ $item = [];
+
+ // Add URI
+ $item['uri'] = $this->extractNewsUri($article);
+ // Add title
+ $item['title'] = $this->extractNewsTitle($article) . ' - ';
+ // Add price to title
+ $item['title'] .= $this->extractNewsDescription4($article);
+ // Add SUPER NABIDKA if it exists
+ if ($this->extractNewsDescription3($article) != '') {
+ $item['title'] .= ' - ' . $this->extractNewsDescription3($article);
+ }
+
+ // Add metric
+ $item['content'] = 'Jednotka: ' . $this->extractNewsDescription($article) . '
';
+ // Add date from and to
+ $item['content'] .= 'Akce: ' . $this->extractNewsDescription1($article) . ' ' . $this->extractNewsDescription2($article) . '
';
+ $item['content'] .= '---
';
+ // Add price
+ $item['content'] .= 'Cena: ' . $this->extractNewsDescription4($article) . '
';
+ // Add price per metric
+ $item['content'] .= 'Cena za jednotku: ' . $this->extractNewsDescription5($article) . '
';
+
+ // Add promotional price if it exists
+ if ($this->extractNewsDescription6($article) != null) {
+ $item['content'] .= '---
';
+ // Add price
+ $item['content'] .= 'Cena s PENNY kartou: ' . $this->extractNewsDescription6($article) . '
';
+ // Add price per metric
+ $item['content'] .= 'Cena za jednotku s PENNY kartou: ' . $this->extractNewsDescription7($article) . '
';
+ }
+
+ // Initialize images array
+ $images = [];
+
+ // Extract images by product code. The product code is in the format XX-XXXXXX
+ $productCode = $this->extractProductCodeFromEnd($item['uri']);
+ if ($productCode !== false && array_key_exists($productCode, $urlField)) {
+ $images[] = $urlField[$this->extractProductCodeFromEnd($item['uri'])];
+
+ $item['enclosures'] = $images;
+ }
+
+ // Add to rss query
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Collects uri, timestamp, title, content and images in the promotional letter from the HTML and transforms to rss.
+ *
+ * @param object $html The HTML object.
+ * @return void
+ */
+ private function collectLetter($html)
+ {
+ // Check if page contains articles and split by class
+ $articles = $html->find('div.ws-column.col-12.flex-column.col-sm-6.col-md-3') or
+ returnServerException('No articles found! Layout might have changed!');
+
+ // Articles loop
+ foreach ($articles as $article) {
+ $item = [];
+
+ // Check if article contains letter
+ // If it doesn't, continue to next article
+ if ($this->extractLetterUri($article) == null) {
+ continue;
+ }
+
+ // Add URI
+ $item['uri'] = $this->extractLetterUri($article);
+ // Add title
+ $item['title'] = 'Promo leták: ' . $this->extractLetterTitle($article);
+ // Add content
+ $item['content'] = $this->extractLetterDescription($article);
+ // Add to rss query
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Extracts the URI of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The URI of the news article.
+ */
+ private function extractNewsUri($article)
+ {
+ // Return URI of the article
+ $element = $article->find('.ws-product-tile__link', 0) or
+ returnServerException('Anchor not found!');
+
+ return $element->href;
+ }
+
+ /**
+ * Extracts the URI of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The URI of the news article.
+ */
+ private function extractLetterUri($article)
+ {
+ // Return URI of the article
+ $element = $article->find('a.ws-btn', 0);
+
+ // Element empty check
+ if ($element == null) {
+ return '';
+ }
+
+ return $element->href;
+ }
+
+ /**
+ * Extracts the date of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The date of the news article.
+ */
+ private function extractNewsDate($article)
+ {
+ // Check if date is set
+ $element = $article->find('div.post__info', 0)->find('span', 0) or
+ returnServerException('Date not found!');
+
+ $date = trim(explode('|', $element->plaintext)[0]);
+
+ // Format date
+ return $this->fixDate($date);
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription($article)
+ {
+ // Extract description
+ $element = $article->find('ul.ws-product-information__piece-description', 0)->find('li', 0) or
+ returnServerException('Description not found!');
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription1($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price-validity', 0)->find('div', 0) or
+ returnServerException('Description not found!');
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription2($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price-validity', 0)->find('div', 1) or
+ returnServerException('Description not found!');
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription3($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-badge-text', 0);
+
+ // Check if element is not null
+ // If it is null, return empty string
+ // If it is not null, return the inner text
+ // This is to avoid errors when the element is not found
+ // and to ensure that the function always returns a string
+ if ($element != null) {
+ return $element->innertext;
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription4($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price-type__value', 0);
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription5($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price-type__label', 0);
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription6($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price', 0)->find('div.ws-product-price-type', 1);
+
+ // Element empty check
+ if ($element == null) {
+ return '';
+ }
+
+ // Not null, so we can safely access the element
+ $element = $element->find('div.ws-product-price-type__value', 0);
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription7($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price', 0)->find('div.ws-product-price-type', 1);
+
+ // Element empty check
+ if ($element == null) {
+ return '';
+ }
+
+ // Not null, so we can safely access the element
+ $element = $element->find('div.ws-product-price-type__label', 0);
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the title of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The title of the news article.
+ */
+ private function extractNewsTitle($article)
+ {
+ // Extract title
+ $element = $article->find('span.show-sr-and-print', 0) or
+ returnServerException('Title not found!');
+
+ return $element->plaintext;
+ }
+
+ /**
+ * Extracts the title of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The title of the news article.
+ */
+ private function extractLetterTitle($article)
+ {
+ // Extract title
+ $element = $article->find('div.ws-text', 0) or
+ returnServerException('Title not found!');
+
+ // Element empty check
+ if ($element == null) {
+ return '';
+ }
+
+ return $element->plaintext;
+ }
+
+ /**
+ * Extracts the description of the letter article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractLetterDescription($article)
+ {
+ // Extract description
+ $element = $article->find('a', 0);
+
+ return $element;
+ }
+
+ /**
+ * It attempts to recognize the date/time format in a string and create a DateTime object.
+ *
+ * It goes through the list of defined formats and tries to apply them to the input string.
+ * Returns the first successfully parsed DateTime object that matches the entire string.
+ *
+ * @param string $dateString A string potentially containing a date and/or time.
+ * @return DateTime|null A DateTime object if successfully recognized and parsed, otherwise null.
+ */
+ private function parseDateTimeFromString(string $dateString): ?DateTime
+ {
+ // List of common formats - YOU CAN AND SHOULD EXPAND IT according to expected inputs!
+ // Order may matter if the formats are ambiguous.
+ // It is recommended to give more specific formats (with time, full year) before more general ones.
+ $possibleFormats = [
+ // Czech formats (day.month.year)
+ 'd.m.Y H:i:s', // 10.04.2025 10:57:47
+ 'j.n.Y H:i:s', // 10.4.2025 10:57:47
+ 'd. m. Y H:i:s', // 10. 04. 2025 10:57:47
+ 'j. n. Y H:i:s', // 10. 4. 2025 10:57:47
+ 'd.m.Y H:i', // 10.04.2025 10:57
+ 'j.n.Y H:i', // 10.4.2025 10:57
+ 'd. m. Y H:i', // 10. 04. 2025 10:57
+ 'j. n. Y H:i', // 10. 4. 2025 10:57
+ 'd.m.Y', // 10.04.2025
+ 'j.n.Y', // 10.4.2025
+ 'd. m. Y', // 10. 04. 2025
+ 'j. n. Y', // 10. 4. 2025
+ // ISO 8601 and international formats (year-month-day)
+ 'Y-m-d H:i:s', // 2025-04-10 10:57:47
+ 'Y-m-d H:i', // 2025-04-10 10:57
+ 'Y-m-d', // 2025-04-10
+ 'YmdHis', // 20250410105747
+ 'Ymd', // 20250410
+ // American formats (month/day/year) - beware of ambiguity!
+ 'm/d/Y H:i:s', // 04/10/2025 10:57:47
+ 'n/j/Y H:i:s', // 4/10/2025 10:57:47
+ 'm/d/Y H:i', // 04/10/2025 10:57
+ 'n/j/Y H:i', // 4/10/2025 10:57
+ 'm/d/Y', // 04/10/2025
+ 'n/j/Y', // 4/10/2025
+ // Standard formats (including time zone)
+ DateTime::ATOM, // example. 2025-04-10T10:57:47+02:00
+ DateTime::RFC3339, // example. 2025-04-10T10:57:47+02:00
+ DateTime::RFC3339_EXTENDED, // example. 2025-04-10T10:57:47.123+02:00
+ DateTime::RFC2822, // example. Thu, 10 Apr 2025 10:57:47 +0200
+ DateTime::ISO8601, // example. 2025-04-10T105747+0200
+ 'Y-m-d\TH:i:sP', // ISO 8601 s 'T' oddělovačem
+ 'Y-m-d\TH:i:s.uP', // ISO 8601 s mikrosekundami
+ // You can add more formats as needed...
+ // e.g. 'd-M-Y' (10-Apr-2025) - requires English locale
+ // e.g. 'j. F Y' (10. abren 2025) - requires Czech locale
+ ];
+
+ // Set locale for parsing month/day names (if using F, M, l, D)
+ // E.g. setlocale(LC_TIME, 'cs_CZ.UTF-8'); or 'en_US.UTF-8');
+
+ foreach ($possibleFormats as $format) {
+ // We will try to create a DateTime object from the given format
+ $dateTime = DateTime::createFromFormat($format, $dateString);
+
+ // We check that the parsing was successful AND ALSO
+ // that there were no errors or warnings during the parsing.
+ // This is important to ensure that the format matches the ENTIRE string.
+ if ($dateTime !== false) {
+ $errors = DateTime::getLastErrors();
+ if (!($errors)) {
+ // Success! We found a valid format for the entire string.
+ return $dateTime;
+ }
+ }
+ }
+
+ // If no format matches or parsing failed
+ return null;
+ }
+
+ /**
+ * Finds values from an associative array whose keys are substrings of a given text.
+ *
+ * The function iterates through the `$map` associative array. For each key,
+ * it checks if that key exists as a substring within the input `$text`.
+ * If found, the corresponding value from the map is added to the result array.
+ * The search is case-sensitive and treats special characters literally.
+ *
+ * @param string $text The input text string to search within.
+ * @param array $map An associative array (key => value). Keys from this array will be searched for in `$text`.
+ * @return array An array of values whose corresponding keys were found as substrings in `$text`. Returns an empty array if no keys are found.
+ */
+ private function findValuesByKeySubstring(string $text, array $map): array
+ {
+ $foundValues = []; // Initialize array for found values
+
+ // Iterate through each key => value pair in the map
+ foreach ($map as $key => $value) {
+ // Use strpos(), which finds the position of the first occurrence of a substring.
+ // Returns the position (including 0) or `false` if the substring is not found.
+ // We use `!== false` to correctly handle the case where the key starts at position 0.
+ // Cast key to string for robustness (though array keys are usually strings or ints).
+ // `strpos` treats special characters in the key and text literally.
+
+ // echo "Key: $key, Text: $text
\n";
+ if (strpos($text, $key) !== false) {
+ // If the key was found in the text, add its corresponding value to the result array
+ $foundValues[] = $value;
+ }
+ }
+
+ // Return the array of found values
+ return $foundValues;
+ }
+
+ /**
+ * Removes Czech diacritics from a given string.
+ *
+ * This function replaces Czech characters with their ASCII equivalents.
+ * For example, 'á' becomes 'a', 'č' becomes 'c', etc.
+ *
+ * @param string $text The input string with Czech diacritics.
+ * @return string The string with Czech diacritics removed.
+ */
+ private function removeCzechDiacritics(string $text): string
+ {
+ $czech = [
+ 'á', 'č', 'ď', 'é', 'ě', 'í', 'ň', 'ó', 'ř', 'š', 'ť', 'ú', 'ů', 'ý', 'ž',
+ 'Á', 'Č', 'Ď', 'É', 'Ě', 'Í', 'Ň', 'Ó', 'Ř', 'Š', 'Ť', 'Ú', 'Ů', 'Ý', 'Ž'
+ ];
+ $ascii = [
+ 'a', 'c', 'd', 'e', 'e', 'i', 'n', 'o', 'r', 's', 't', 'u', 'u', 'y', 'z',
+ 'A', 'C', 'D', 'E', 'E', 'I', 'N', 'O', 'R', 'S', 'T', 'U', 'U', 'Y', 'Z'
+ ];
+
+ return str_replace($czech, $ascii, $text);
+ }
+
+ /**
+ * Extracts a product code from a given URL.
+ *
+ * This method attempts to retrieve a product code by:
+ * 1. Using a regular expression to match the pattern XX-XXXXXX in the URL.
+ * 2. Alternatively, splitting the URL by dashes and checking if the first two parts are numeric.
+ *
+ * @param string $url The URL from which to extract the product code.
+ * @return string|false The extracted product code if found, or false if no valid code is present.
+ */
+ private function extractProductCode($url)
+ {
+ // Use the regular expression to find the pattern XX-XXXXXX in the last part of the URL
+ if (preg_match('/\/(\d{2}-\d{6})-/', $url, $matches)) {
+ return $matches[1]; // Returns the first captured group
+ }
+
+ // Alternative approach with expansion and parsing
+ $parts = explode('/', $url);
+ $lastPart = end($parts);
+
+ // Divide last part by dash
+ $elements = explode('-', $lastPart);
+
+ // If we have the right format, the first two parts should be "88" and "200897"
+ if (count($elements) >= 3 && is_numeric($elements[0]) && is_numeric($elements[1])) {
+ return $elements[0] . '-' . $elements[1];
+ }
+
+ return false; // Returns false if the code has not been found
+ }
+
+
+ /**
+ * Extracts a numerical product code from the end of a given URL.
+ *
+ * This method attempts to retrieve a product code by:
+ * 1. Using a regular expression to match a numeric sequence at the end of the URL.
+ * 2. Alternatively, splitting the URL by dashes and checking if the last segment is numeric.
+ *
+ * @param string $url The URL from which to extract the product code.
+ * @return string|false The extracted product code if found, or false if no valid code is present.
+ */
+ private function extractProductCodeFromEnd($url)
+ {
+ // Using preg _ match to find the number at the end of the URL
+ if (preg_match('/-(\d+)$/', $url, $matches)) {
+ return $matches[1];
+ }
+
+ // Alternative approach - divide the URL by dash and take the last part
+ $parts = explode('-', $url);
+ $lastPart = end($parts);
+
+ // If the last part is numerical, return it
+ if (is_numeric($lastPart)) {
+ return $lastPart;
+ }
+
+ return false; // Return false if code not found
+ }
+
+
+ // endregion
+}
+
diff --git a/bridges/StavebninyDEKBridge.php b/bridges/StavebninyDEKBridge.php
new file mode 100644
index 00000000000..1461f65f74c
--- /dev/null
+++ b/bridges/StavebninyDEKBridge.php
@@ -0,0 +1,640 @@
+ [],
+ 'Events' => [],
+ 'Topic' => []
+ ];
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ defaultLinkTo($html, static::URI);
+
+ // Router
+ switch ($this->queriedContext) {
+ case 'News':
+ $this->collectNews($html);
+ break;
+ case 'Events':
+ $this->collectEvents($html);
+ break;
+ case 'Topic':
+ $this->collectTopic($html);
+ break;
+ }
+ }
+
+ /**
+ * Returns the icon for the bridge.
+ *
+ * @return string The icon URL.
+ */
+ public function getURI()
+ {
+ $uri = static::URI;
+
+ // URI Router
+ switch ($this->queriedContext) {
+ case 'News':
+ $uri .= 'akce/nabidka/';
+ break;
+ case 'Events':
+ $uri .= 'pobocka-praha-hostivar/akce/udalosti/';
+ break;
+ case 'Topic':
+ $uri .= 'pobocka-praha-hostivar/akce/temata/';
+ break;
+ }
+
+ return $uri;
+ }
+
+ /**
+ * Returns the keyword URL map for the bridge.
+ *
+ * @return string The Name.
+ */
+ public function getKeywordUrlMap()
+ {
+ // Get the keyword URL map from the class constant
+ $keywordUrlMap = static::KEYWORDURLMAP;
+
+ // returns the keyword URL map
+ return $keywordUrlMap;
+ }
+
+ /**
+ * Returns the name for the bridge.
+ *
+ * @return string The Name.
+ */
+ public function getName()
+ {
+ $name = static::NAME;
+
+ $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : '';
+
+ switch ($this->queriedContext) {
+ case 'News':
+ break;
+ case 'Events':
+ break;
+ case 'Topic':
+ break;
+ }
+
+ return $name;
+ }
+
+ /**
+ * Parse most used date formats
+ *
+ * Basically strtotime doesn't convert dates correctly due to formats
+ * being hard to interpret. So we use the DateTime object, manually
+ * fixing dates and times (set to 00:00:00.000).
+ *
+ * We don't know the timezone, so just assume +00:00 (or whatever
+ * DateTime chooses)
+ */
+ private function fixDate($date)
+ {
+ $df = $this->parseDateTimeFromString($date);
+
+ return date_format($df, 'U');
+ }
+
+ /**
+ * Extracts the images from the article.
+ *
+ * @param object $article The article object.
+ * @return array An array of image URLs.
+ */
+ private function extractImages($article)
+ {
+ // Notice: We can have zero or more images (though it should mostly be 1)
+ $elements = $article->find('img');
+
+ $images = [];
+
+ foreach ($elements as $img) {
+ $images[] = $img->src;
+ }
+
+ return $images;
+ }
+
+ // region Weekly offer
+
+ /**
+ * Collects uri, timestamp, title, content and images in the product offers from the HTML and transforms to rss.
+ *
+ * @param object $html The HTML object.
+ * @return void
+ */
+ private function collectNews($html)
+ {
+ // Check if page contains articles and split by class
+ $articles = $html->find('.com-news-feature-prerex') or
+ returnServerException('No articles found! Layout might have changed!');
+
+ // Articles loop
+ foreach ($articles as $article) {
+ $item = [];
+
+ // Add URI
+ $item['uri'] = $this->extractNewsUri($article);
+// echo $item['uri'] . '
';
+ // Add title
+ $item['title'] = $this->extractNewsTitle($article);
+// echo $item['title'] . '
';
+ $item['enclosures'] = $this->extractImages($article);
+
+ // Add to rss query
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Collects uri, timestamp, title, content and images in the promotional letter from the HTML and transforms to rss.
+ *
+ * @param object $html The HTML object.
+ * @return void
+ */
+ private function collectEvents($html)
+ {
+ // Check if page contains articles and split by class
+ $articles = $html->find('.com-news-common-prerex') or
+ returnServerException('No articles found! Layout might have changed!');
+
+ // Articles loop
+ foreach ($articles as $article) {
+ $item = [];
+
+ // Add URI
+ $item['uri'] = $this->extractEventUri($article);
+ // Add title
+ $item['title'] = $this->extractEventTitle($article);
+ // Add content
+ $item['content'] = $this->extractEventDescription($article);
+ // Parse time
+ $newsDate = $this->extractDate($article);
+ // Remove prefix
+ $newsDate = str_replace('zveřejněno: ', '', $newsDate);
+ // Fix date
+ $item['timestamp'] = $this->fixDate($newsDate);
+ // Add images
+ $item['enclosures'] = $this->extractImages($article);
+
+ // Add to rss query
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Collects uri, timestamp, title, content and images in the promotional letter from the HTML and transforms to rss.
+ *
+ * @param object $html The HTML object.
+ * @return void
+ */
+ private function collectTopic($html)
+ {
+ // Check if page contains articles and split by class
+ $articles = $html->find('.com-news-common-prerex') or
+ returnServerException('No articles found! Layout might have changed!');
+
+ // Articles loop
+ foreach ($articles as $article) {
+ $item = [];
+
+ // Add URI
+ $item['uri'] = $this->extractEventUri($article);
+ // Add title
+ $item['title'] = $this->extractEventTitle($article);
+ // Add content
+ $item['content'] = $this->extractEventDescription($article);
+ // Parse time
+ $newsDate = $this->extractDate($article);
+ // Remove prefix
+ $newsDate = str_replace('zveřejněno: ', '', $newsDate);
+ // Fix date
+ $item['timestamp'] = $this->fixDate($newsDate);
+ // Add images
+ $item['enclosures'] = $this->extractImages($article);
+
+ // Add to rss query
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Extracts the URI of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The URI of the news article.
+ */
+ private function extractEventUri($article)
+ {
+ return $article->href;
+ }
+
+
+ /**
+ * Extracts the URI of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The URI of the news article.
+ */
+ private function extractNewsUri($article)
+ {
+ // Return URI of the article
+ $element = $article->find('a', 0) or
+ returnServerException('Anchor not found!');
+
+ return $element->href;
+ }
+
+ /**
+ * Extracts the URI of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The URI of the news article.
+ */
+ private function extractLetterUri($article)
+ {
+ // Return URI of the article
+ $element = $article->find('a.ws-btn', 0);
+
+ // Element empty check
+ if ($element == null) {
+ return '';
+ }
+
+ return $element->href;
+ }
+
+ /**
+ * Extracts the date of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The date of the news article.
+ */
+ private function extractDate($article)
+ {
+ // Check if date is set
+ $element = $article->find('div.com-news-common-prerex__date', 0) or
+ returnServerException('Date not found!');
+
+ return $element->plaintext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription($article)
+ {
+ // Extract description
+ $element = $article->find('ul.ws-product-information__piece-description', 0)->find('li', 0) or
+ returnServerException('Description not found!');
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription1($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price-validity', 0)->find('div', 0) or
+ returnServerException('Description not found!');
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription2($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price-validity', 0)->find('div', 1) or
+ returnServerException('Description not found!');
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription3($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-badge-text', 0);
+
+ // Check if element is not null
+ // If it is null, return empty string
+ // If it is not null, return the inner text
+ // This is to avoid errors when the element is not found
+ // and to ensure that the function always returns a string
+ if ($element != null) {
+ return $element->innertext;
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription4($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price-type__value', 0);
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription5($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price-type__label', 0);
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription6($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price', 0)->find('div.ws-product-price-type', 1);
+
+ // Element empty check
+ if ($element == null) {
+ return '';
+ }
+
+ // Not null, so we can safely access the element
+ $element = $element->find('div.ws-product-price-type__value', 0);
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractEventDescription($article)
+ {
+ // Extract description
+ $element = $article->find('.com-news-common-prerex__text', 0);
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the title of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The title of the news article.
+ */
+ private function extractNewsTitle($article)
+ {
+ // Extract title
+ $element = $article->find('img', 0) or
+ returnServerException('Title not found!');
+
+ return $element->alt;
+ }
+
+ /**
+ * Extracts the title of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The title of the news article.
+ */
+ private function extractEventTitle($article)
+ {
+ // Extract title
+ $element = $article->find('div.com-news-common-prerex__right-box', 0)->find('h3', 0)
+ or returnServerException('Title not found!');
+
+ return $element->plaintext;
+ }
+
+ /**
+ * Extracts the description of the letter article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractLetterDescription($article)
+ {
+ // Extract description
+ $element = $article->find('a', 0);
+
+ return $element;
+ }
+
+ /**
+ * It attempts to recognize the date/time format in a string and create a DateTime object.
+ *
+ * It goes through the list of defined formats and tries to apply them to the input string.
+ * Returns the first successfully parsed DateTime object that matches the entire string.
+ *
+ * @param string $dateString A string potentially containing a date and/or time.
+ * @return DateTime|null A DateTime object if successfully recognized and parsed, otherwise null.
+ */
+ private function parseDateTimeFromString(string $dateString): ?DateTime
+ {
+ // List of common formats - YOU CAN AND SHOULD EXPAND IT according to expected inputs!
+ // Order may matter if the formats are ambiguous.
+ // It is recommended to give more specific formats (with time, full year) before more general ones.
+ $possibleFormats = [
+ // Czech formats (day.month.year)
+ 'd.m.Y H:i:s', // 10.04.2025 10:57:47
+ 'j.n.Y H:i:s', // 10.4.2025 10:57:47
+ 'd. m. Y H:i:s', // 10. 04. 2025 10:57:47
+ 'j. n. Y H:i:s', // 10. 4. 2025 10:57:47
+ 'd.m.Y H:i', // 10.04.2025 10:57
+ 'j.n.Y H:i', // 10.4.2025 10:57
+ 'd. m. Y H:i', // 10. 04. 2025 10:57
+ 'j. n. Y H:i', // 10. 4. 2025 10:57
+ 'd.m.Y', // 10.04.2025
+ 'j.n.Y', // 10.4.2025
+ 'd. m. Y', // 10. 04. 2025
+ 'j. n. Y', // 10. 4. 2025
+ // ISO 8601 and international formats (year-month-day)
+ 'Y-m-d H:i:s', // 2025-04-10 10:57:47
+ 'Y-m-d H:i', // 2025-04-10 10:57
+ 'Y-m-d', // 2025-04-10
+ 'YmdHis', // 20250410105747
+ 'Ymd', // 20250410
+ // American formats (month/day/year) - beware of ambiguity!
+ 'm/d/Y H:i:s', // 04/10/2025 10:57:47
+ 'n/j/Y H:i:s', // 4/10/2025 10:57:47
+ 'm/d/Y H:i', // 04/10/2025 10:57
+ 'n/j/Y H:i', // 4/10/2025 10:57
+ 'm/d/Y', // 04/10/2025
+ 'n/j/Y', // 4/10/2025
+ // Standard formats (including time zone)
+ DateTime::ATOM, // example. 2025-04-10T10:57:47+02:00
+ DateTime::RFC3339, // example. 2025-04-10T10:57:47+02:00
+ DateTime::RFC3339_EXTENDED, // example. 2025-04-10T10:57:47.123+02:00
+ DateTime::RFC2822, // example. Thu, 10 Apr 2025 10:57:47 +0200
+ DateTime::ISO8601, // example. 2025-04-10T105747+0200
+ 'Y-m-d\TH:i:sP', // ISO 8601 s 'T' oddělovačem
+ 'Y-m-d\TH:i:s.uP', // ISO 8601 s mikrosekundami
+ // You can add more formats as needed...
+ // e.g. 'd-M-Y' (10-Apr-2025) - requires English locale
+ // e.g. 'j. F Y' (10. abren 2025) - requires Czech locale
+ ];
+
+ // Set locale for parsing month/day names (if using F, M, l, D)
+ // E.g. setlocale(LC_TIME, 'cs_CZ.UTF-8'); or 'en_US.UTF-8');
+
+ foreach ($possibleFormats as $format) {
+ // We will try to create a DateTime object from the given format
+ $dateTime = DateTime::createFromFormat($format, $dateString);
+
+ // We check that the parsing was successful AND ALSO
+ // that there were no errors or warnings during the parsing.
+ // This is important to ensure that the format matches the ENTIRE string.
+ if ($dateTime !== false) {
+ $errors = DateTime::getLastErrors();
+ if (!($errors)) {
+ // Success! We found a valid format for the entire string.
+ return $dateTime;
+ }
+ }
+ }
+
+ // If no format matches or parsing failed
+ return null;
+ }
+
+ /**
+ * Finds values from an associative array whose keys are substrings of a given text.
+ *
+ * The function iterates through the `$map` associative array. For each key,
+ * it checks if that key exists as a substring within the input `$text`.
+ * If found, the corresponding value from the map is added to the result array.
+ * The search is case-sensitive and treats special characters literally.
+ *
+ * @param string $text The input text string to search within.
+ * @param array $map An associative array (key => value). Keys from this array will be searched for in `$text`.
+ * @return array An array of values whose corresponding keys were found as substrings in `$text`. Returns an empty array if no keys are found.
+ */
+ private function findValuesByKeySubstring(string $text, array $map): array
+ {
+ $foundValues = []; // Initialize array for found values
+
+ // Iterate through each key => value pair in the map
+ foreach ($map as $key => $value) {
+ // Use strpos(), which finds the position of the first occurrence of a substring.
+ // Returns the position (including 0) or `false` if the substring is not found.
+ // We use `!== false` to correctly handle the case where the key starts at position 0.
+ // Cast key to string for robustness (though array keys are usually strings or ints).
+ // `strpos` treats special characters in the key and text literally.
+
+ // echo "Key: $key, Text: $text
\n";
+ if (strpos($text, $key) !== false) {
+ // If the key was found in the text, add its corresponding value to the result array
+ $foundValues[] = $value;
+ }
+ }
+
+ // Return the array of found values
+ return $foundValues;
+ }
+
+ /**
+ * Removes Czech diacritics from a given string.
+ *
+ * This function replaces Czech characters with their ASCII equivalents.
+ * For example, 'á' becomes 'a', 'č' becomes 'c', etc.
+ *
+ * @param string $text The input string with Czech diacritics.
+ * @return string The string with Czech diacritics removed.
+ */
+ private function removeCzechDiacritics(string $text): string
+ {
+ $czech = [
+ 'á', 'č', 'ď', 'é', 'ě', 'í', 'ň', 'ó', 'ř', 'š', 'ť', 'ú', 'ů', 'ý', 'ž',
+ 'Á', 'Č', 'Ď', 'É', 'Ě', 'Í', 'Ň', 'Ó', 'Ř', 'Š', 'Ť', 'Ú', 'Ů', 'Ý', 'Ž'
+ ];
+ $ascii = [
+ 'a', 'c', 'd', 'e', 'e', 'i', 'n', 'o', 'r', 's', 't', 'u', 'u', 'y', 'z',
+ 'A', 'C', 'D', 'E', 'E', 'I', 'N', 'O', 'R', 'S', 'T', 'U', 'U', 'Y', 'Z'
+ ];
+
+ return str_replace($czech, $ascii, $text);
+ }
+
+ // endregion
+
+ /**
+ * formatTitleFromURI
+ */
+ private function formatTitleFromURI(string $uri): string
+ {
+ // get last part of the URI
+ $title = basename($uri);
+
+ // Pattern: /[^\p{L}\p{N}]+/u
+ // [^...] - Match any character NOT in the set
+ // \p{L} - Any Unicode letter (including 'é', 'ü', 'ñ', etc.)
+ // \p{N} - Any Unicode number (0-9 and other numeric characters)
+ // + - Match one or more occurrences of the preceding pattern consecutively
+ // /u - Unicode modifier, essential for \p{} constructs
+ $pattern = '/[^\p{L}\p{N}]+/u';
+ $replacement = ' '; // Replace with a single space
+
+ // lets replace
+ $title = preg_replace($pattern, $replacement, $title);
+
+ // first letter to uppercase
+ $title = ucfirst($title);
+
+ return trim((string)$title);
+ }
+}
\ No newline at end of file
diff --git a/bridges/StavebninyIZOMATBridge.php b/bridges/StavebninyIZOMATBridge.php
new file mode 100644
index 00000000000..ac9567f1268
--- /dev/null
+++ b/bridges/StavebninyIZOMATBridge.php
@@ -0,0 +1,729 @@
+ [],
+ 'Blog' => []
+// 'Promo' => []
+ ];
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ defaultLinkTo($html, static::URI);
+
+ // Router
+ switch ($this->queriedContext) {
+ case 'News':
+ $this->collectNews($html);
+ break;
+ case 'Blog':
+ $this->collectBlog($html);
+ break;
+ case 'Promo':
+ $this->collectPromo($html);
+ break;
+ }
+ }
+
+ /**
+ * Returns the icon for the bridge.
+ *
+ * @return string The icon URL.
+ */
+ public function getURI()
+ {
+ $uri = static::URI;
+
+ // URI Router
+ switch ($this->queriedContext) {
+ case 'News':
+ $uri .= '/aktuality/';
+ break;
+ case 'Blog':
+ $uri .= '/blog/';
+ break;
+ case 'Promo':
+ $uri .= '/';
+ break;
+ }
+
+ return $uri;
+ }
+
+ /**
+ * Returns the keyword URL map for the bridge.
+ *
+ * @return string The Name.
+ */
+ public function getKeywordUrlMap()
+ {
+ // Get the keyword URL map from the class constant
+ $keywordUrlMap = static::KEYWORDURLMAP;
+
+ // returns the keyword URL map
+ return $keywordUrlMap;
+ }
+
+ /**
+ * Returns the name for the bridge.
+ *
+ * @return string The Name.
+ */
+ public function getName()
+ {
+ $name = static::NAME;
+
+ $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : '';
+
+ switch ($this->queriedContext) {
+ case 'News':
+ break;
+ case 'Blog':
+ break;
+ case 'Promo':
+ break;
+ }
+
+ return $name;
+ }
+
+ /**
+ * Parse most used date formats
+ *
+ * Basically strtotime doesn't convert dates correctly due to formats
+ * being hard to interpret. So we use the DateTime object, manually
+ * fixing dates and times (set to 00:00:00.000).
+ *
+ * We don't know the timezone, so just assume +00:00 (or whatever
+ * DateTime chooses)
+ */
+ private function fixDate($date)
+ {
+ echo $date;
+ $df = $this->parseDateTimeFromString($date);
+ echo $df;
+ exit;
+
+ return date_format($df, 'U');
+ }
+
+ /**
+ * Extracts the images from the article.
+ *
+ * @param object $article The article object.
+ * @return array An array of image URLs.
+ */
+ private function extractImages($article)
+ {
+ // Notice: We can have zero or more images (though it should mostly be 1)
+ $elements = $article->find('img');
+
+ $images = [];
+
+ foreach ($elements as $img) {
+ $images[] = $img->src;
+ }
+
+ return $images;
+ }
+
+ // region Weekly offer
+
+ /**
+ * Collects uri, timestamp, title, content and images in the product offers from the HTML and transforms to rss.
+ *
+ * @param object $html The HTML object.
+ * @return void
+ */
+ private function collectNews($html)
+ {
+ // Check if page contains articles and split by class
+ $articles = $html->find('.col-12.col-md-6') or
+ returnServerException('No articles found! Layout might have changed!');
+
+ // Articles loop
+ foreach ($articles as $article) {
+ $item = [];
+
+ if (!($this->isValidUrl($this->extractNewsUri($article)))) {
+ continue;
+ }
+
+ // Add URI
+ $item['uri'] = $this->extractNewsUri($article);
+
+ // Add title
+ $item['title'] = $this->extractNewsTitle($article);
+
+ // Add content
+ $item['content'] = $this->extractNewsDescription($article);
+
+ // Add images
+ $imgset = [];
+ $imgset[] = $this->extractNewsIMG($article);
+ $item['enclosures'] = $imgset;
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Collects uri, timestamp, title, content and images in the promotional letter from the HTML and transforms to rss.
+ *
+ * @param object $html The HTML object.
+ * @return void
+ */
+ private function collectBlog($html)
+ {
+ // Check if page contains articles and split by class
+ $articles = $html->find('div.col-12.col-md-6') or
+ returnServerException('No articles found! Layout might have changed!');
+
+ // Articles loop
+ foreach ($articles as $article) {
+ $item = [];
+
+ // Check if page contains articles and split by class
+ if (!($this->isValidUrl($this->extractNewsUri($article)))) {
+ continue;
+ }
+
+ // Add URI
+ $item['uri'] = $this->extractNewsUri($article);
+
+ // Add title
+ $item['title'] = $this->extractBlogTitle($article);
+
+ // Add content
+ $item['content'] = $this->extractBlogDescription($article);
+
+ // Add images
+ $imgset = [];
+ $imgset[] = $this->extractNewsIMG($article);
+ $item['enclosures'] = $imgset;
+
+ // Add to rss query
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Collects uri, timestamp, title, content and images in the promotional letter from the HTML and transforms to rss.
+ *
+ * @param object $html The HTML object.
+ * @return void
+ */
+ private function collectPromo($html)
+ {
+ // Check if page contains articles and split by class
+ $articles = $html->find('div.product-wrap') or
+ returnServerException('No articles found! Layout might have changed!');
+
+ // Articles loop
+ foreach ($articles as $article) {
+ $item = [];
+
+ // Add URI
+ $item['uri'] = $this->extractNewsUri($article);
+ echo $item['uri'] . '
';
+/*
+ // Add title
+ $item['title'] = $this->extractEventTitle($article);
+ // Add content
+ $item['content'] = $this->extractEventDescription($article);
+ // Parse time
+ $newsDate = $this->extractDate($article);
+ // Remove prefix
+ $newsDate = str_replace("zveřejněno: ", "", $newsDate);
+ // Fix date
+ $item['timestamp'] = $this->fixDate($newsDate);
+ // Add images
+ $item['enclosures'] = $this->extractImages($article);
+
+ // Add to rss query
+ $this->items[] = $item;
+*/
+ }
+ }
+
+ /**
+ * Extracts the IMG of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The URI of the news article.
+ */
+ private function extractNewsIMG($article)
+ {
+ // Return URI of the article
+ $element = $article->find('.img-fluid', 0);
+
+ // Element empty check
+ if ($element == null) {
+ return '';
+ }
+
+ return $element->getAttribute('data-src');
+ }
+
+ /**
+ * Extracts the IMG of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The URI of the news article.
+ */
+ private function extractBlogIMG($article)
+ {
+ // Return URI of the article
+ $element = $article->find('.img-fluid.loaded', 0);
+
+ // Element empty check
+ if ($element == null) {
+ return '';
+ }
+
+ return $element->getAttribute('data-src');
+ }
+
+ /**
+ * Extracts the URI of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The URI of the news article.
+ */
+ private function extractNewsUri($article)
+ {
+ // Return URI of the article
+ $element = $article->find('a', 0);
+
+ // Element empty check
+ if ($element == null) {
+ return '';
+ }
+
+ return $element->href;
+ }
+
+ /**
+ * Extracts the URI of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The URI of the news article.
+ */
+ private function extractLetterUri($article)
+ {
+ // Return URI of the article
+ $element = $article->find('a.ws-btn', 0);
+
+ // Element empty check
+ if ($element == null) {
+ return '';
+ }
+
+ return $element->href;
+ }
+
+ /**
+ * Extracts the date of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The date of the news article.
+ */
+ private function extractDate($article)
+ {
+ // Check if date is set
+ $element = $article->find('div.blog-article__date', 0) or
+ returnServerException('Date not found!');
+
+ return $element->plaintext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription1($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price-validity', 0)->find('div', 0) or
+ returnServerException('Description not found!');
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractBlogDescription($article)
+ {
+ // Extract description
+ $element = $article->find('p.blog-article__text', 0) or
+ returnServerException('Description not found!');
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription2($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price-validity', 0)->find('div', 1) or
+ returnServerException('Description not found!');
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription3($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-badge-text', 0);
+
+ // Check if element is not null
+ // If it is null, return empty string
+ // If it is not null, return the inner text
+ // This is to avoid errors when the element is not found
+ // and to ensure that the function always returns a string
+ if ($element != null) {
+ return $element->innertext;
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription4($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price-type__value', 0);
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription5($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price-type__label', 0);
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription6($article)
+ {
+ // Extract description
+ $element = $article->find('div.ws-product-price', 0)->find('div.ws-product-price-type', 1);
+
+ // Element empty check
+ if ($element == null) {
+ return '';
+ }
+
+ // Not null, so we can safely access the element
+ $element = $element->find('div.ws-product-price-type__value', 0);
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription($article)
+ {
+ // Extract description
+ $element = $article->find('.blog-article__text.content-text.card-text.articles-desc', 0);
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the title of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The title of the news article.
+ */
+ private function extractBlogTitle($article)
+ {
+ // Extract title
+ $element = $article->find('a.blog-article__link', 0) or
+ returnServerException('Title not found!');
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the title of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The title of the news article.
+ */
+ private function extractNewsTitle($article)
+ {
+ // Extract title
+ $element = $article->find('a.blog-article__link', 0) or
+ returnServerException('Title not found!');
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the title of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The title of the news article.
+ */
+ private function extractEventTitle($article)
+ {
+ // Extract title
+ $element = $article->find('div.com-news-common-prerex__right-box', 0)->find('h3', 0)
+ or returnServerException('Title not found!');
+
+ return $element->plaintext;
+ }
+
+ /**
+ * Extracts the description of the letter article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractLetterDescription($article)
+ {
+ // Extract description
+ $element = $article->find('a', 0);
+
+ return $element;
+ }
+
+ /**
+ * It attempts to recognize the date/time format in a string and create a DateTime object.
+ *
+ * It goes through the list of defined formats and tries to apply them to the input string.
+ * Returns the first successfully parsed DateTime object that matches the entire string.
+ *
+ * @param string $dateString A string potentially containing a date and/or time.
+ * @return DateTime|null A DateTime object if successfully recognized and parsed, otherwise null.
+ */
+ private function parseDateTimeFromString(string $dateString): ?DateTime
+ {
+ // List of common formats - YOU CAN AND SHOULD EXPAND IT according to expected inputs!
+ // Order may matter if the formats are ambiguous.
+ // It is recommended to give more specific formats (with time, full year) before more general ones.
+ $possibleFormats = [
+ // Czech formats (day.month.year)
+ 'd.m.Y H:i:s', // 10.04.2025 10:57:47
+ 'j.n.Y H:i:s', // 10.4.2025 10:57:47
+ 'd. m. Y H:i:s', // 10. 04. 2025 10:57:47
+ 'j. n. Y H:i:s', // 10. 4. 2025 10:57:47
+ 'd.m.Y H:i', // 10.04.2025 10:57
+ 'j.n.Y H:i', // 10.4.2025 10:57
+ 'd. m. Y H:i', // 10. 04. 2025 10:57
+ 'j. n. Y H:i', // 10. 4. 2025 10:57
+ 'd.m.Y', // 10.04.2025
+ 'j.n.Y', // 10.4.2025
+ 'd. m. Y', // 10. 04. 2025
+ 'j. n. Y', // 10. 4. 2025
+ // ISO 8601 and international formats (year-month-day)
+ 'Y-m-d H:i:s', // 2025-04-10 10:57:47
+ 'Y-m-d H:i', // 2025-04-10 10:57
+ 'Y-m-d', // 2025-04-10
+ 'YmdHis', // 20250410105747
+ 'Ymd', // 20250410
+ // American formats (month/day/year) - beware of ambiguity!
+ 'm/d/Y H:i:s', // 04/10/2025 10:57:47
+ 'n/j/Y H:i:s', // 4/10/2025 10:57:47
+ 'm/d/Y H:i', // 04/10/2025 10:57
+ 'n/j/Y H:i', // 4/10/2025 10:57
+ 'm/d/Y', // 04/10/2025
+ 'n/j/Y', // 4/10/2025
+ // Standard formats (including time zone)
+ DateTime::ATOM, // example. 2025-04-10T10:57:47+02:00
+ DateTime::RFC3339, // example. 2025-04-10T10:57:47+02:00
+ DateTime::RFC3339_EXTENDED, // example. 2025-04-10T10:57:47.123+02:00
+ DateTime::RFC2822, // example. Thu, 10 Apr 2025 10:57:47 +0200
+ DateTime::ISO8601, // example. 2025-04-10T105747+0200
+ 'Y-m-d\TH:i:sP', // ISO 8601 s 'T' oddělovačem
+ 'Y-m-d\TH:i:s.uP', // ISO 8601 s mikrosekundami
+ // You can add more formats as needed...
+ // e.g. 'd-M-Y' (10-Apr-2025) - requires English locale
+ // e.g. 'j. F Y' (10. abren 2025) - requires Czech locale
+ ];
+
+ // Set locale for parsing month/day names (if using F, M, l, D)
+ // E.g. setlocale(LC_TIME, 'cs_CZ.UTF-8'); or 'en_US.UTF-8');
+
+ foreach ($possibleFormats as $format) {
+ // We will try to create a DateTime object from the given format
+ $dateTime = DateTime::createFromFormat($format, $dateString);
+
+ // We check that the parsing was successful AND ALSO
+ // that there were no errors or warnings during the parsing.
+ // This is important to ensure that the format matches the ENTIRE string.
+ if ($dateTime !== false) {
+ $errors = DateTime::getLastErrors();
+ if (!($errors)) {
+ // Success! We found a valid format for the entire string.
+ return $dateTime;
+ }
+ }
+ }
+
+ // If no format matches or parsing failed
+ return null;
+ }
+
+ /**
+ * Finds values from an associative array whose keys are substrings of a given text.
+ *
+ * The function iterates through the `$map` associative array. For each key,
+ * it checks if that key exists as a substring within the input `$text`.
+ * If found, the corresponding value from the map is added to the result array.
+ * The search is case-sensitive and treats special characters literally.
+ *
+ * @param string $text The input text string to search within.
+ * @param array $map An associative array (key => value). Keys from this array will be searched for in `$text`.
+ * @return array An array of values whose corresponding keys were found as substrings in `$text`. Returns an empty array if no keys are found.
+ */
+ private function findValuesByKeySubstring(string $text, array $map): array
+ {
+ $foundValues = []; // Initialize array for found values
+
+ // Iterate through each key => value pair in the map
+ foreach ($map as $key => $value) {
+ // Use strpos(), which finds the position of the first occurrence of a substring.
+ // Returns the position (including 0) or `false` if the substring is not found.
+ // We use `!== false` to correctly handle the case where the key starts at position 0.
+ // Cast key to string for robustness (though array keys are usually strings or ints).
+ // `strpos` treats special characters in the key and text literally.
+
+ // echo "Key: $key, Text: $text
\n";
+ if (strpos($text, $key) !== false) {
+ // If the key was found in the text, add its corresponding value to the result array
+ $foundValues[] = $value;
+ }
+ }
+
+ // Return the array of found values
+ return $foundValues;
+ }
+
+ /**
+ * Removes Czech diacritics from a given string.
+ *
+ * This function replaces Czech characters with their ASCII equivalents.
+ * For example, 'á' becomes 'a', 'č' becomes 'c', etc.
+ *
+ * @param string $text The input string with Czech diacritics.
+ * @return string The string with Czech diacritics removed.
+ */
+ private function removeCzechDiacritics(string $text): string
+ {
+ $czech = [
+ 'á', 'č', 'ď', 'é', 'ě', 'í', 'ň', 'ó', 'ř', 'š', 'ť', 'ú', 'ů', 'ý', 'ž',
+ 'Á', 'Č', 'Ď', 'É', 'Ě', 'Í', 'Ň', 'Ó', 'Ř', 'Š', 'Ť', 'Ú', 'Ů', 'Ý', 'Ž'
+ ];
+ $ascii = [
+ 'a', 'c', 'd', 'e', 'e', 'i', 'n', 'o', 'r', 's', 't', 'u', 'u', 'y', 'z',
+ 'A', 'C', 'D', 'E', 'E', 'I', 'N', 'O', 'R', 'S', 'T', 'U', 'U', 'Y', 'Z'
+ ];
+
+ return str_replace($czech, $ascii, $text);
+ }
+
+ // endregion
+
+ /**
+ * formatTitleFromURI
+ */
+ private function formatTitleFromURI(string $uri): string
+ {
+ // get last part of the URI
+ $title = basename($uri);
+
+ // Pattern: /[^\p{L}\p{N}]+/u
+ // [^...] - Match any character NOT in the set
+ // \p{L} - Any Unicode letter (including 'é', 'ü', 'ñ', etc.)
+ // \p{N} - Any Unicode number (0-9 and other numeric characters)
+ // + - Match one or more occurrences of the preceding pattern consecutively
+ // /u - Unicode modifier, essential for \p{} constructs
+ $pattern = '/[^\p{L}\p{N}]+/u';
+ $replacement = ' '; // Replace with a single space
+
+ // lets replace
+ $title = preg_replace($pattern, $replacement, $title);
+
+ // first letter to uppercase
+ $title = ucfirst($title);
+
+ return trim((string)$title);
+ }
+
+ /**
+ * Checks if the given string is a valid URL.
+ *
+ * This function uses the PHP filter FILTER_VALIDATE_URL.
+ * It requires the presence of a scheme (e.g., "http://", "https://") for successful validation.
+ *
+ * @param string $url The string to be checked.
+ * @return bool Returns true if the string is a valid URL, false otherwise.
+ */
+ private function isValidUrl(string $url): bool
+ {
+ // Use filter_var with the FILTER_VALIDATE_URL filter.
+ // This function returns the filtered URL on success (which is not false),
+ // or false on validation failure.
+ // Comparing with !== false ensures returning an actual boolean true/false value.
+ return filter_var($url, FILTER_VALIDATE_URL) !== false;
+ }
+}
\ No newline at end of file
diff --git a/bridges/StavimbydlimBridge.php b/bridges/StavimbydlimBridge.php
new file mode 100644
index 00000000000..9182b9b3ebd
--- /dev/null
+++ b/bridges/StavimbydlimBridge.php
@@ -0,0 +1,288 @@
+ [
+ ],
+ ];
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ defaultLinkTo($html, static::URI);
+
+ // Router
+ switch ($this->queriedContext) {
+ case 'Articles':
+ $this->collectNews($html);
+ break;
+ }
+ }
+
+ /**
+ * Returns the icon for the bridge.
+ *
+ * @return string The icon URL.
+ */
+ public function getURI()
+ {
+ $uri = static::URI;
+
+ // URI Router
+ switch ($this->queriedContext) {
+ case 'Articles':
+ $uri .= '/';
+ break;
+ }
+
+ return $uri;
+ }
+
+ /**
+ * Returns the name for the bridge.
+ *
+ * @return string The Name.
+ */
+ public function getName()
+ {
+ $name = static::NAME;
+
+ $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : '';
+
+ switch ($this->queriedContext) {
+ case 'Articles':
+ break;
+ }
+
+ return $name;
+ }
+
+ /**
+ * Parse most used date formats
+ *
+ * Basically strtotime doesn't convert dates correctly due to formats
+ * being hard to interpret. So we use the DateTime object, manually
+ * fixing dates and times (set to 00:00:00.000).
+ *
+ * We don't know the timezone, so just assume +00:00 (or whatever
+ * DateTime chooses)
+ */
+ private function fixDate($date)
+ {
+ $df = $this->parseDateTimeFromString($date);
+
+ return date_format($df, 'U');
+ }
+
+ /**
+ * Extracts the images from the article.
+ *
+ * @param object $article The article object.
+ * @return array An array of image URLs.
+ */
+ private function extractImages($article)
+ {
+ // Notice: We can have zero or more images (though it should mostly be 1)
+ $elements = $article->find('img');
+
+ $images = [];
+
+ foreach ($elements as $img) {
+ $images[] = $img->src;
+ }
+
+ return $images;
+ }
+
+ #region Articles
+
+ /**
+ * Collects uri, timestamp, title, content and images in the news articles from the HTML and transforms to rss.
+ *
+ * @param object $html The HTML object.
+ * @return void
+ */
+ private function collectNews($html)
+ {
+ // Check if page contains articles
+ $articles = $html->find('.post.type-post.status-publish.format-standard')
+ or returnServerException('No articles found! Layout might have changed!');
+
+ foreach ($articles as $article) {
+ $item = [];
+
+ $item['uri'] = $this->extractNewsUri($article);
+ // Add title
+ $item['title'] = $this->extractNewsTitle($article);
+ // Add description
+ $item['content'] = $this->extractNewsDescription($article);
+ // Add images
+ $item['enclosures'] = $this->extractImages($article);
+ // Add timestamp
+ $item['timestamp'] = $this->extractNewsDate($article);
+
+ // collect sources into rss article
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Extracts the URI of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The URI of the news article.
+ */
+ private function extractNewsUri($article)
+ {
+ // Return URI of the article
+ $element = $article->find('a', 0)
+ or returnServerException('Anchor not found!');
+
+ return $element->href;
+ }
+
+ /**
+ * Extracts the date of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The date of the news article.
+ */
+ private function extractNewsDate($article)
+ {
+ // Check if date is set
+ $element = $article->find('time.entry-date.published', 0)
+ or returnServerException('Date not found!');
+
+ // Format date
+ return $this->fixDate($element->plaintext);
+ }
+
+ /**
+ * Extracts the description of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The description of the news article.
+ */
+ private function extractNewsDescription($article)
+ {
+ // Extract description
+ $element = $article->find('div.entry-summary', 0)->find('p', 0)
+ or returnServerException('Description not found!');
+
+ return $element->innertext;
+ }
+
+ /**
+ * Extracts the title of the news article.
+ *
+ * @param object $article The article object.
+ * @return string The title of the news article.
+ */
+ private function extractNewsTitle($article)
+ {
+ // Extract title
+ $element = $article->find('a', 0)
+ or returnServerException('Title not found!');
+
+ return $element->plaintext;
+ }
+
+ /**
+ * It attempts to recognize the date/time format in a string and create a DateTime object.
+ *
+ * It goes through the list of defined formats and tries to apply them to the input string.
+ * Returns the first successfully parsed DateTime object that matches the entire string.
+ *
+ * @param string $dateString A string potentially containing a date and/or time.
+ * @return DateTime|null A DateTime object if successfully recognized and parsed, otherwise null.
+ */
+ private function parseDateTimeFromString(string $dateString): ?DateTime
+ {
+ // List of common formats - YOU CAN AND SHOULD EXPAND IT according to expected inputs!
+ // Order may matter if the formats are ambiguous.
+ // It is recommended to give more specific formats (with time, full year) before more general ones.
+ $possibleFormats = [
+ // Czech formats (day.month.year)
+ 'd.m.Y H:i:s', // 10.04.2025 10:57:47
+ 'j.n.Y H:i:s', // 10.4.2025 10:57:47
+ 'd. m. Y H:i:s', // 10. 04. 2025 10:57:47
+ 'j. n. Y H:i:s', // 10. 4. 2025 10:57:47
+ 'd.m.Y H:i', // 10.04.2025 10:57
+ 'j.n.Y H:i', // 10.4.2025 10:57
+ 'd. m. Y H:i', // 10. 04. 2025 10:57
+ 'j. n. Y H:i', // 10. 4. 2025 10:57
+ 'd.m.Y', // 10.04.2025
+ 'j.n.Y', // 10.4.2025
+ 'd. m. Y', // 10. 04. 2025
+ 'j. n. Y', // 10. 4. 2025
+
+ // ISO 8601 and international formats (year-month-day)
+ 'Y-m-d H:i:s', // 2025-04-10 10:57:47
+ 'Y-m-d H:i', // 2025-04-10 10:57
+ 'Y-m-d', // 2025-04-10
+ 'YmdHis', // 20250410105747
+ 'Ymd', // 20250410
+
+ // American formats (month/day/year) - beware of ambiguity!
+ 'm/d/Y H:i:s', // 04/10/2025 10:57:47
+ 'n/j/Y H:i:s', // 4/10/2025 10:57:47
+ 'm/d/Y H:i', // 04/10/2025 10:57
+ 'n/j/Y H:i', // 4/10/2025 10:57
+ 'm/d/Y', // 04/10/2025
+ 'n/j/Y', // 4/10/2025
+
+ // Standard formats (including time zone)
+ DateTime::ATOM, // example. 2025-04-10T10:57:47+02:00
+ DateTime::RFC3339, // example. 2025-04-10T10:57:47+02:00
+ DateTime::RFC3339_EXTENDED, // example. 2025-04-10T10:57:47.123+02:00
+ DateTime::RFC2822, // example. Thu, 10 Apr 2025 10:57:47 +0200
+ DateTime::ISO8601, // example. 2025-04-10T105747+0200
+ 'Y-m-d\TH:i:sP', // ISO 8601 s 'T' oddělovačem
+ 'Y-m-d\TH:i:s.uP', // ISO 8601 s mikrosekundami
+
+ // You can add more formats as needed...
+ // e.g. 'd-M-Y' (10-Apr-2025) - requires English locale
+ // e.g. 'j. F Y' (10. abren 2025) - requires Czech locale
+ ];
+
+ // Set locale for parsing month/day names (if using F, M, l, D)
+ // E.g. setlocale(LC_TIME, 'cs_CZ.UTF-8'); or 'en_US.UTF-8');
+
+ foreach ($possibleFormats as $format) {
+ // We will try to create a DateTime object from the given format
+ $dateTime = DateTime::createFromFormat($format, $dateString);
+
+ // We check that the parsing was successful AND ALSO
+ // that there were no errors or warnings during the parsing.
+ // This is important to ensure that the format matches the ENTIRE string.
+ if ($dateTime !== false) {
+ $errors = DateTime::getLastErrors();
+ if (!($errors)) {
+ // Success! We found a valid format for the entire string.
+ return $dateTime;
+ }
+ }
+ }
+
+ // If no format matches or parsing failed
+ return null;
+ }
+
+ #endregion
+}
\ No newline at end of file