Compare commits

..

No commits in common. "cf34de67729c28666b2c4e46a60064e543611379" and "af98fc10795599f886e512176fd91e40545f4c43" have entirely different histories.

8 changed files with 168 additions and 602 deletions

View file

@ -33,7 +33,6 @@
"jbtronics/dompdf-font-loader-bundle": "^1.0.0", "jbtronics/dompdf-font-loader-bundle": "^1.0.0",
"jbtronics/settings-bundle": "^3.0.0", "jbtronics/settings-bundle": "^3.0.0",
"jfcherng/php-diff": "^6.14", "jfcherng/php-diff": "^6.14",
"jkphl/micrometa": "dev-master",
"knpuniversity/oauth2-client-bundle": "^2.15", "knpuniversity/oauth2-client-bundle": "^2.15",
"league/commonmark": "^2.7", "league/commonmark": "^2.7",
"league/csv": "^9.8.0", "league/csv": "^9.8.0",
@ -160,12 +159,6 @@
"App\\Tests\\": "tests/" "App\\Tests\\": "tests/"
} }
}, },
"repositories": [
{
"type": "vcs",
"url": "https://github.com/jbtronics/micrometa"
}
],
"scripts": { "scripts": {
"auto-scripts": { "auto-scripts": {
"cache:clear": "symfony-cmd", "cache:clear": "symfony-cmd",

466
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "7c76e3af5fd042105a3208fdcb300a11", "content-hash": "699f421ad81f8a1acacf8e2c4af66491",
"packages": [ "packages": [
{ {
"name": "amphp/amp", "name": "amphp/amp",
@ -3883,16 +3883,16 @@
}, },
{ {
"name": "doctrine/migrations", "name": "doctrine/migrations",
"version": "3.9.7", "version": "3.9.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/migrations.git", "url": "https://github.com/doctrine/migrations.git",
"reference": "96cb2a89b56c9efb0bac38e606dc0b0f13e650ec" "reference": "ffd8355cdd8505fc650d9604f058bf62aedd80a1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/migrations/zipball/96cb2a89b56c9efb0bac38e606dc0b0f13e650ec", "url": "https://api.github.com/repos/doctrine/migrations/zipball/ffd8355cdd8505fc650d9604f058bf62aedd80a1",
"reference": "96cb2a89b56c9efb0bac38e606dc0b0f13e650ec", "reference": "ffd8355cdd8505fc650d9604f058bf62aedd80a1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3966,7 +3966,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/migrations/issues", "issues": "https://github.com/doctrine/migrations/issues",
"source": "https://github.com/doctrine/migrations/tree/3.9.7" "source": "https://github.com/doctrine/migrations/tree/3.9.6"
}, },
"funding": [ "funding": [
{ {
@ -3982,7 +3982,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-04-23T19:33:20+00:00" "time": "2026-02-11T06:46:11+00:00"
}, },
{ {
"name": "doctrine/orm", "name": "doctrine/orm",
@ -4074,20 +4074,19 @@
}, },
{ {
"name": "doctrine/persistence", "name": "doctrine/persistence",
"version": "4.2.0", "version": "4.1.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/persistence.git", "url": "https://github.com/doctrine/persistence.git",
"reference": "49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b" "reference": "b9c49ad3558bb77ef973f4e173f2e9c2eca9be09"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/persistence/zipball/49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b", "url": "https://api.github.com/repos/doctrine/persistence/zipball/b9c49ad3558bb77ef973f4e173f2e9c2eca9be09",
"reference": "49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b", "reference": "b9c49ad3558bb77ef973f4e173f2e9c2eca9be09",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"doctrine/deprecations": "^1",
"doctrine/event-manager": "^1 || ^2", "doctrine/event-manager": "^1 || ^2",
"php": "^8.1", "php": "^8.1",
"psr/cache": "^1.0 || ^2.0 || ^3.0" "psr/cache": "^1.0 || ^2.0 || ^3.0"
@ -4098,13 +4097,13 @@
"phpstan/phpstan-phpunit": "^2", "phpstan/phpstan-phpunit": "^2",
"phpstan/phpstan-strict-rules": "^2", "phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "^10.5.58 || ^12", "phpunit/phpunit": "^10.5.58 || ^12",
"symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0 || ^8.0", "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0",
"symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0 || ^8.0" "symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Doctrine\\Persistence\\": "src" "Doctrine\\Persistence\\": "src/Persistence"
} }
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
@ -4148,7 +4147,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/persistence/issues", "issues": "https://github.com/doctrine/persistence/issues",
"source": "https://github.com/doctrine/persistence/tree/4.2.0" "source": "https://github.com/doctrine/persistence/tree/4.1.1"
}, },
"funding": [ "funding": [
{ {
@ -4164,7 +4163,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-04-26T12:12:52+00:00" "time": "2025-10-16T20:13:18+00:00"
}, },
{ {
"name": "doctrine/sql-formatter", "name": "doctrine/sql-formatter",
@ -5535,191 +5534,6 @@
], ],
"time": "2023-05-21T07:57:08+00:00" "time": "2023-05-21T07:57:08+00:00"
}, },
{
"name": "jkphl/dom-factory",
"version": "v1.0.1",
"source": {
"type": "git",
"url": "https://github.com/jkphl/dom-factory.git",
"reference": "dd32b8b2cc800f065c0eff8bb621d9f80147d45e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jkphl/dom-factory/zipball/dd32b8b2cc800f065c0eff8bb621d9f80147d45e",
"reference": "dd32b8b2cc800f065c0eff8bb621d9f80147d45e",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"guzzlehttp/guzzle": "^6.0||^7.0",
"masterminds/html5": "^2.7",
"php": ">=7.2"
},
"require-dev": {
"clue/graph-composer": "^1.1",
"php-coveralls/php-coveralls": "^2.2",
"phpunit/phpunit": "^8.0||^9.0",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Jkphl\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Joschi Kuphal",
"email": "joschi@kuphal.net",
"homepage": "https://jkphl.is",
"role": "Developer"
}
],
"description": "Simple HTML5/XML DOM factory",
"homepage": "https://github.com/jkphl/dom-factory",
"support": {
"email": "joschi@kuphal.net",
"issues": "https://github.com/jkphl/dom-factory/issues",
"source": "https://github.com/jkphl/dom-factory"
},
"time": "2021-06-28T11:49:36+00:00"
},
{
"name": "jkphl/micrometa",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/jbtronics/micrometa.git",
"reference": "720f409151c2cc20add9478b7a0a635fa1707021"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jbtronics/micrometa/zipball/720f409151c2cc20add9478b7a0a635fa1707021",
"reference": "720f409151c2cc20add9478b7a0a635fa1707021",
"shasum": ""
},
"require": {
"ext-dom": "*",
"jkphl/dom-factory": "^1",
"jkphl/rdfa-lite-microdata": "^0.4.4",
"league/uri": "^5.0|^6.5|^7.0",
"mf2/mf2": "^0.4",
"ml/json-ld": "^1.2",
"monolog/monolog": "^1.24 || ^2 || ^3",
"php": ">=7.1.3",
"psr/cache": "^1.0|^2|^3",
"psr/log": "^1.1|^2|^3",
"symfony/cache": "^4.0|^5.0|^6.0|^7.0|^8.0"
},
"require-dev": {
"clue/graph-composer": "^1.1",
"mf2/tests": "@dev",
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^7.0 || ^8.5",
"squizlabs/php_codesniffer": "^3.3"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"Jkphl\\": "src/"
}
},
"scripts": {
"phpunit": [
"vendor/bin/phpunit --configuration phpunit.xml.dist"
],
"depgraph": [
"vendor/bin/graph-composer --no-dev export . doc/dependencies.svg"
],
"check-style": [
"vendor/bin/phpcs -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src"
],
"fix-style": [
"vendor/bin/phpcbf -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src"
],
"test": [
"@phpunit"
]
},
"license": [
"MIT"
],
"authors": [
{
"name": "Joschi Kuphal",
"email": "joschi@tollwerk.de",
"homepage": "https://jkphl.is",
"role": "Developer"
}
],
"description": "A meta parser for extracting micro information out of web documents, currently supporting Microformats 1+2, HTML Microdata, RDFa Lite 1.1 and JSON-LD",
"homepage": "https://jkphl.is/projects/micrometa/",
"support": {
"email": "joschi@tollwerk.de",
"source": "https://github.com/jkphl/micrometa",
"issues": "https://github.com/jkphl/micrometa/issues"
},
"time": "2026-04-26T17:25:19+00:00"
},
{
"name": "jkphl/rdfa-lite-microdata",
"version": "v0.4.7",
"source": {
"type": "git",
"url": "https://github.com/jkphl/rdfa-lite-microdata.git",
"reference": "ffc4940e8be55798257a03da7ed7d4506a13c3e5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jkphl/rdfa-lite-microdata/zipball/ffc4940e8be55798257a03da7ed7d4506a13c3e5",
"reference": "ffc4940e8be55798257a03da7ed7d4506a13c3e5",
"shasum": ""
},
"require": {
"jkphl/dom-factory": "^1",
"php": ">=5.5"
},
"require-dev": {
"clue/graph-composer": "dev-master",
"codeclimate/php-test-reporter": "^0.4.4",
"phpunit/phpunit": "^4.8",
"satooshi/php-coveralls": "^1.0",
"squizlabs/php_codesniffer": "^2.8"
},
"type": "library",
"autoload": {
"psr-4": {
"Jkphl\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Joschi Kuphal",
"email": "joschi@tollwerk.de",
"homepage": "https://jkphl.is",
"role": "Developer"
}
],
"description": "RDFa Lite 1.1 and HTML Microdata parser for web documents (HTML, SVG, XML)",
"homepage": "https://github.com/jkphl/rdfa-lite-microdata",
"support": {
"email": "joschi@tollwerk.de",
"issues": "https://github.com/jkphl/rdfa-lite-microdata/issues",
"source": "https://github.com/jkphl/rdfa-lite-microdata"
},
"time": "2023-01-27T13:29:45+00:00"
},
{ {
"name": "kelunik/certificate", "name": "kelunik/certificate",
"version": "v1.1.3", "version": "v1.1.3",
@ -7085,170 +6899,6 @@
}, },
"time": "2025-07-25T09:04:22+00:00" "time": "2025-07-25T09:04:22+00:00"
}, },
{
"name": "mf2/mf2",
"version": "0.4.6",
"source": {
"type": "git",
"url": "https://github.com/microformats/php-mf2.git",
"reference": "00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/microformats/php-mf2/zipball/00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23",
"reference": "00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"mf2/tests": "@dev",
"phpdocumentor/phpdocumentor": "v2.8.4",
"phpunit/phpunit": "4.8.*"
},
"suggest": {
"barnabywalters/mf-cleaner": "To more easily handle the canonical data php-mf2 gives you",
"masterminds/html5": "Alternative HTML parser for PHP, for better HTML5 support."
},
"bin": [
"bin/fetch-mf2",
"bin/parse-mf2"
],
"type": "library",
"autoload": {
"files": [
"Mf2/Parser.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"CC0-1.0"
],
"authors": [
{
"name": "Barnaby Walters",
"homepage": "http://waterpigs.co.uk"
}
],
"description": "A pure, generic microformats2 parser — makes HTML as easy to consume as a JSON API",
"keywords": [
"html",
"microformats",
"microformats 2",
"parser",
"semantic"
],
"support": {
"issues": "https://github.com/microformats/php-mf2/issues",
"source": "https://github.com/microformats/php-mf2/tree/master"
},
"time": "2018-08-24T14:47:04+00:00"
},
{
"name": "ml/iri",
"version": "1.1.4",
"target-dir": "ML/IRI",
"source": {
"type": "git",
"url": "https://github.com/lanthaler/IRI.git",
"reference": "cbd44fa913e00ea624241b38cefaa99da8d71341"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lanthaler/IRI/zipball/cbd44fa913e00ea624241b38cefaa99da8d71341",
"reference": "cbd44fa913e00ea624241b38cefaa99da8d71341",
"shasum": ""
},
"require": {
"lib-pcre": ">=4.0",
"php": ">=5.3.0"
},
"type": "library",
"autoload": {
"psr-0": {
"ML\\IRI": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Markus Lanthaler",
"email": "mail@markus-lanthaler.com",
"homepage": "http://www.markus-lanthaler.com",
"role": "Developer"
}
],
"description": "IRI handling for PHP",
"homepage": "http://www.markus-lanthaler.com",
"keywords": [
"URN",
"iri",
"uri",
"url"
],
"support": {
"issues": "https://github.com/lanthaler/IRI/issues",
"source": "https://github.com/lanthaler/IRI/tree/master"
},
"time": "2014-01-21T13:43:39+00:00"
},
{
"name": "ml/json-ld",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/lanthaler/JsonLD.git",
"reference": "537e68e87a6bce23e57c575cd5dcac1f67ce25d8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lanthaler/JsonLD/zipball/537e68e87a6bce23e57c575cd5dcac1f67ce25d8",
"reference": "537e68e87a6bce23e57c575cd5dcac1f67ce25d8",
"shasum": ""
},
"require": {
"ext-json": "*",
"ml/iri": "^1.1.1",
"php": ">=5.3.0"
},
"require-dev": {
"json-ld/tests": "1.0",
"phpunit/phpunit": "^4"
},
"type": "library",
"autoload": {
"psr-4": {
"ML\\JsonLD\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Markus Lanthaler",
"email": "mail@markus-lanthaler.com",
"homepage": "http://www.markus-lanthaler.com",
"role": "Developer"
}
],
"description": "JSON-LD Processor for PHP",
"homepage": "http://www.markus-lanthaler.com",
"keywords": [
"JSON-LD",
"jsonld"
],
"support": {
"issues": "https://github.com/lanthaler/JsonLD/issues",
"source": "https://github.com/lanthaler/JsonLD/tree/1.2.1"
},
"time": "2022-09-29T08:45:17+00:00"
},
{ {
"name": "monolog/monolog", "name": "monolog/monolog",
"version": "3.10.0", "version": "3.10.0",
@ -9759,16 +9409,16 @@
}, },
{ {
"name": "rhukster/dom-sanitizer", "name": "rhukster/dom-sanitizer",
"version": "1.0.11", "version": "1.0.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/rhukster/dom-sanitizer.git", "url": "https://github.com/rhukster/dom-sanitizer.git",
"reference": "02d08ec8b36b93b04517d74fe82b715ef06273bd" "reference": "49a98046b708a4c92f754f5b0ef1720bb85142e2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/rhukster/dom-sanitizer/zipball/02d08ec8b36b93b04517d74fe82b715ef06273bd", "url": "https://api.github.com/repos/rhukster/dom-sanitizer/zipball/49a98046b708a4c92f754f5b0ef1720bb85142e2",
"reference": "02d08ec8b36b93b04517d74fe82b715ef06273bd", "reference": "49a98046b708a4c92f754f5b0ef1720bb85142e2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -9798,9 +9448,9 @@
"description": "A simple but effective DOM/SVG/MathML Sanitizer for PHP 7.4+", "description": "A simple but effective DOM/SVG/MathML Sanitizer for PHP 7.4+",
"support": { "support": {
"issues": "https://github.com/rhukster/dom-sanitizer/issues", "issues": "https://github.com/rhukster/dom-sanitizer/issues",
"source": "https://github.com/rhukster/dom-sanitizer/tree/1.0.11" "source": "https://github.com/rhukster/dom-sanitizer/tree/1.0.10"
}, },
"time": "2026-04-23T22:56:32+00:00" "time": "2026-04-10T17:00:11+00:00"
}, },
{ {
"name": "robrichards/xmlseclibs", "name": "robrichards/xmlseclibs",
@ -14043,7 +13693,7 @@
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.37.0", "version": "v1.36.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git", "url": "https://github.com/symfony/polyfill-ctype.git",
@ -14102,7 +13752,7 @@
"portable" "portable"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" "source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0"
}, },
"funding": [ "funding": [
{ {
@ -14126,16 +13776,16 @@
}, },
{ {
"name": "symfony/polyfill-intl-grapheme", "name": "symfony/polyfill-intl-grapheme",
"version": "v1.37.0", "version": "v1.36.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
"reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -14184,7 +13834,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.36.0"
}, },
"funding": [ "funding": [
{ {
@ -14204,11 +13854,11 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-04-26T13:13:48+00:00" "time": "2026-04-10T16:19:22+00:00"
}, },
{ {
"name": "symfony/polyfill-intl-icu", "name": "symfony/polyfill-intl-icu",
"version": "v1.37.0", "version": "v1.36.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-icu.git", "url": "https://github.com/symfony/polyfill-intl-icu.git",
@ -14272,7 +13922,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.37.0" "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.36.0"
}, },
"funding": [ "funding": [
{ {
@ -14296,7 +13946,7 @@
}, },
{ {
"name": "symfony/polyfill-intl-idn", "name": "symfony/polyfill-intl-idn",
"version": "v1.37.0", "version": "v1.36.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git", "url": "https://github.com/symfony/polyfill-intl-idn.git",
@ -14359,7 +14009,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0" "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.36.0"
}, },
"funding": [ "funding": [
{ {
@ -14383,7 +14033,7 @@
}, },
{ {
"name": "symfony/polyfill-intl-normalizer", "name": "symfony/polyfill-intl-normalizer",
"version": "v1.37.0", "version": "v1.36.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@ -14444,7 +14094,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.36.0"
}, },
"funding": [ "funding": [
{ {
@ -14468,7 +14118,7 @@
}, },
{ {
"name": "symfony/polyfill-php83", "name": "symfony/polyfill-php83",
"version": "v1.37.0", "version": "v1.36.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php83.git", "url": "https://github.com/symfony/polyfill-php83.git",
@ -14524,7 +14174,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0" "source": "https://github.com/symfony/polyfill-php83/tree/v1.36.0"
}, },
"funding": [ "funding": [
{ {
@ -14548,7 +14198,7 @@
}, },
{ {
"name": "symfony/polyfill-php84", "name": "symfony/polyfill-php84",
"version": "v1.37.0", "version": "v1.36.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php84.git", "url": "https://github.com/symfony/polyfill-php84.git",
@ -14604,7 +14254,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0" "source": "https://github.com/symfony/polyfill-php84/tree/v1.36.0"
}, },
"funding": [ "funding": [
{ {
@ -14628,16 +14278,16 @@
}, },
{ {
"name": "symfony/polyfill-php85", "name": "symfony/polyfill-php85",
"version": "v1.37.0", "version": "v1.36.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php85.git", "url": "https://github.com/symfony/polyfill-php85.git",
"reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee" "reference": "2c408a6bb0313e6001a83628dc5506100474254e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee", "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e",
"reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee", "reference": "2c408a6bb0313e6001a83628dc5506100474254e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -14684,7 +14334,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0" "source": "https://github.com/symfony/polyfill-php85/tree/v1.36.0"
}, },
"funding": [ "funding": [
{ {
@ -14704,11 +14354,11 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-04-26T13:10:57+00:00" "time": "2026-04-10T16:50:15+00:00"
}, },
{ {
"name": "symfony/polyfill-uuid", "name": "symfony/polyfill-uuid",
"version": "v1.37.0", "version": "v1.36.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-uuid.git", "url": "https://github.com/symfony/polyfill-uuid.git",
@ -14767,7 +14417,7 @@
"uuid" "uuid"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" "source": "https://github.com/symfony/polyfill-uuid/tree/v1.36.0"
}, },
"funding": [ "funding": [
{ {
@ -20204,12 +19854,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git", "url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "08cd07f04fb07fb4d316e956801d57b700cf7096" "reference": "10b8a93511210c9bae3be31f4fe13c3ff974cad4"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/08cd07f04fb07fb4d316e956801d57b700cf7096", "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/10b8a93511210c9bae3be31f4fe13c3ff974cad4",
"reference": "08cd07f04fb07fb4d316e956801d57b700cf7096", "reference": "10b8a93511210c9bae3be31f4fe13c3ff974cad4",
"shasum": "" "shasum": ""
}, },
"conflict": { "conflict": {
@ -20232,7 +19882,6 @@
"alextselegidis/easyappointments": "<=1.5.2", "alextselegidis/easyappointments": "<=1.5.2",
"alexusmai/laravel-file-manager": "<=3.3.1", "alexusmai/laravel-file-manager": "<=3.3.1",
"algolia/algoliasearch-magento-2": "<=3.16.1|>=3.17.0.0-beta1,<=3.17.1", "algolia/algoliasearch-magento-2": "<=3.16.1|>=3.17.0.0-beta1,<=3.17.1",
"almirhodzic/nova-toggle-5": "<1.3",
"alt-design/alt-redirect": "<1.6.4", "alt-design/alt-redirect": "<1.6.4",
"altcha-org/altcha": "<1.3.1", "altcha-org/altcha": "<1.3.1",
"alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1",
@ -20329,7 +19978,7 @@
"ckeditor/ckeditor": "<4.25", "ckeditor/ckeditor": "<4.25",
"clickstorm/cs-seo": ">=6,<6.8|>=7,<7.5|>=8,<8.4|>=9,<9.3", "clickstorm/cs-seo": ">=6,<6.8|>=7,<7.5|>=8,<8.4|>=9,<9.3",
"co-stack/fal_sftp": "<0.2.6", "co-stack/fal_sftp": "<0.2.6",
"cockpit-hq/cockpit": "<2.14", "cockpit-hq/cockpit": "<2.13.5",
"code16/sharp": "<9.20", "code16/sharp": "<9.20",
"codeception/codeception": "<3.1.3|>=4,<4.1.22", "codeception/codeception": "<3.1.3|>=4,<4.1.22",
"codeigniter/framework": "<3.1.10", "codeigniter/framework": "<3.1.10",
@ -20492,7 +20141,7 @@
"fisharebest/webtrees": "<=2.1.18", "fisharebest/webtrees": "<=2.1.18",
"fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2", "fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2",
"fixpunkt/fp-newsletter": "<1.1.1|>=1.2,<2.1.2|>=2.2,<3.2.6", "fixpunkt/fp-newsletter": "<1.1.1|>=1.2,<2.1.2|>=2.2,<3.2.6",
"flarum/core": "<=1.8.15|>=2.0.0.0-beta1,<=2.0.0.0-beta8", "flarum/core": "<1.8.10",
"flarum/flarum": "<0.1.0.0-beta8", "flarum/flarum": "<0.1.0.0-beta8",
"flarum/framework": "<1.8.10", "flarum/framework": "<1.8.10",
"flarum/mentions": "<1.6.3", "flarum/mentions": "<1.6.3",
@ -20529,7 +20178,7 @@
"geshi/geshi": "<=1.0.9.1", "geshi/geshi": "<=1.0.9.1",
"getformwork/formwork": "<=2.3.3", "getformwork/formwork": "<=2.3.3",
"getgrav/grav": "<1.11.0.0-beta1", "getgrav/grav": "<1.11.0.0-beta1",
"getkirby/cms": "<5.4", "getkirby/cms": "<=5.2.1",
"getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1", "getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1",
"getkirby/panel": "<2.5.14", "getkirby/panel": "<2.5.14",
"getkirby/starterkit": "<=3.7.0.2", "getkirby/starterkit": "<=3.7.0.2",
@ -20627,7 +20276,7 @@
"kelvinmo/simplexrd": "<3.1.1", "kelvinmo/simplexrd": "<3.1.1",
"kevinpapst/kimai2": "<1.16.7", "kevinpapst/kimai2": "<1.16.7",
"khodakhah/nodcms": "<=3.4.1", "khodakhah/nodcms": "<=3.4.1",
"kimai/kimai": "<2.54", "kimai/kimai": "<=2.53",
"kitodo/presentation": "<3.2.3|>=3.3,<3.3.4", "kitodo/presentation": "<3.2.3|>=3.3,<3.3.4",
"klaviyo/magento2-extension": ">=1,<3", "klaviyo/magento2-extension": ">=1,<3",
"knplabs/knp-snappy": "<=1.4.2", "knplabs/knp-snappy": "<=1.4.2",
@ -21071,7 +20720,7 @@
"twig/twig": "<3.11.2|>=3.12,<3.14.1|>=3.16,<3.19", "twig/twig": "<3.11.2|>=3.12,<3.14.1|>=3.16,<3.19",
"typicms/core": "<16.1.7", "typicms/core": "<16.1.7",
"typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2", "typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2",
"typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1|==14.2", "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1",
"typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", "typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2",
"typo3/cms-beuser": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", "typo3/cms-beuser": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
"typo3/cms-core": "<=8.7.56|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", "typo3/cms-core": "<=8.7.56|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1",
@ -21253,7 +20902,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-04-24T17:22:29+00:00" "time": "2026-04-22T18:27:19+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",
@ -22769,7 +22418,6 @@
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": { "stability-flags": {
"jkphl/micrometa": 20,
"roave/security-advisories": 20 "roave/security-advisories": 20
}, },
"prefer-stable": false, "prefer-stable": false,

View file

@ -48,34 +48,26 @@ final class DTOJsonSchemaConverter
'type' => 'object', 'type' => 'object',
'properties' => [ 'properties' => [
'name' => ['type' => 'string', 'description' => 'Product name'], 'name' => ['type' => 'string', 'description' => 'Product name'],
'description' => ['type' => 'string', 'description' => 'A short description of the product, maybe containing the most important things. Onnly One line.'], 'description' => ['type' => 'string', 'description' => 'Product description'],
'manufacturer' => ['type' => ['string', 'null'], 'description' => 'Manufacturer name'], 'manufacturer' => ['type' => ['string', 'null'], 'description' => 'Manufacturer name'],
'mpn' => ['type' => ['string', 'null'], 'description' => 'Manufacturer Part Number'], 'mpn' => ['type' => ['string', 'null'], 'description' => 'Manufacturer Part Number'],
'category' => ['type' => ['string', 'null'], 'description' => 'Product category, e.g. "Passive components -> Resistors"'], 'category' => ['type' => ['string', 'null'], 'description' => 'Product category'],
'manufacturing_status' => ['type' => ['string', 'null'], 'enum' => ['active', 'obsolete', 'nrfnd', 'discontinued', null], 'description' => 'Manufacturing status'], 'manufacturing_status' => ['type' => ['string', 'null'], 'enum' => ['active', 'obsolete', 'nrfnd', 'discontinued', null], 'description' => 'Manufacturing status'],
'footprint' => ['type' => ['string', 'null'], 'description' => 'Package/footprint type, like "SOT-23", "DIP-8", "QFN-32" etc.'], 'footprint' => ['type' => ['string', 'null'], 'description' => 'Package/footprint type'],
'mass' => ['type' => ['number', 'null'], 'description' => 'Mass of the product in grams'], 'mass' => ['type' => ['number', 'null'], 'description' => 'Mass in grams'],
'gtin' => ['type' => ['string', 'null'], 'description' => 'Global Trade Item Number (GTIN) / EAN / UPC code for barcodes'],
'notes' => ['type' => ['string', 'null'], 'description' => 'Optional long description of the part with more details than description. Can be markdown formatted.'],
'parameters' => [ 'parameters' => [
'type' => 'array', 'type' => 'array',
'items' => [ 'items' => [
'type' => 'object', 'type' => 'object',
'properties' => [ 'properties' => [
'name' => ['type' => 'string'], 'name' => ['type' => 'string'],
'symbol' => ['type' => ['string', 'null'], 'description' => 'An optional quantity symbol for the parameter in latex code, like R_1'], 'value' => ['type' => 'string'],
'value_typical' => ['type' => ['number', 'null'], 'description' => 'The typical value of the parameter. For example, for a resistor this could be 100 for a 100 Ohm resistor. Also used if only one numeric value is given. If used an unit should be given'], 'unit' => ['type' => ['string', 'null']],
'value_min' => ['type' => ['number', 'null'], 'description' => 'If a range is given for the parameter, this is the minimum value. Null if no range is given.'],
'value_max' => ['type' => ['number', 'null'], 'description' => 'If a range is given for the parameter, this is the maximum value. Null if not a range.'],
'value_text' => ['type' => ['string', 'null'], 'description' => 'When a value is not numeric it can be put here as text. Only use if it does not fit in value_min, value_typical or value_max. E.g. "Yes", "Red", etc.'],
'group' => ['type' => ['string', 'null'], 'description' => 'An optional group name for the parameter, e.g. "Electrical parameters", "Mechanical parameters" etc.'],
'unit' => ['type' => ['string', 'null'], 'description' => 'The unit of the parameter values, e.g. kg, Ohm, V, etc.'],
], ],
'required' => ['name', 'value_typical', 'value_min', 'value_max', 'value_text'] 'required' => ['name', 'value'],
], ],
], ],
'datasheets' => [ 'datasheets' => [
'description' => 'A list of datasheets, manuals, or other technical documents related to the product. Not images, but actual documents, preferably PDFs.',
'type' => 'array', 'type' => 'array',
'items' => [ 'items' => [
'type' => 'object', 'type' => 'object',
@ -102,18 +94,17 @@ final class DTOJsonSchemaConverter
'items' => [ 'items' => [
'type' => 'object', 'type' => 'object',
'properties' => [ 'properties' => [
'distributor_name' => ['type' => 'string', 'description' => 'Name of the distributor or vendor. Typically the shop name'], 'distributor_name' => ['type' => 'string'],
'order_number' => ['type' => ['string', 'null'], 'description' => 'The order number or SKU used by the distributor. Optional, but can help to find the product on the distributor website.'], 'order_number' => ['type' => ['string', 'null']],
'product_url' => ['type' => 'string'], 'product_url' => ['type' => 'string'],
'prices_include_vat' => ['type' => ['boolean', 'null'], 'description' => 'Whether the prices include VAT or not. Null if unknown.'],
'prices' => [ 'prices' => [
'type' => 'array', 'type' => 'array',
'items' => [ 'items' => [
'type' => 'object', 'type' => 'object',
'properties' => [ 'properties' => [
'minimum_quantity' => ['type' => 'integer', 'description' => 'Minimum quantity for this price tier. 1 when no tiered pricing is available.'], 'minimum_quantity' => ['type' => 'integer'],
'price' => ['type' => 'number', 'description' => 'Price for the given minimum quantity.'], 'price' => ['type' => 'number'],
'currency' => ['type' => 'string', 'description' => 'Currency ISO code, e.g. USD'], 'currency' => ['type' => 'string'],
], ],
'required' => ['minimum_quantity', 'price', 'currency'], 'required' => ['minimum_quantity', 'price', 'currency'],
], ],
@ -151,15 +142,13 @@ final class DTOJsonSchemaConverter
$parameters = []; $parameters = [];
foreach ($data['parameters'] as $p) { foreach ($data['parameters'] as $p) {
if (!empty($p['name'])) { if (!empty($p['name'])) {
$parameters[] = new ParameterDTO( $value = $p['value'] ?? '';
$unit = $p['unit'] ?? null;
// Combine value and unit for parsing
$valueWithUnit = $unit ? $value . ' ' . $unit : $value;
$parameters[] = ParameterDTO::parseValueField(
name: $p['name'], name: $p['name'],
value_text: $p['value_text'] ?? null, value: $valueWithUnit
value_typ: isset($p['value_typical']) && is_numeric($p['value_typical']) ? (float) $p['value_typical'] : null,
value_min: isset($p['value_min']) && is_numeric($p['value_min']) ? (float) $p['value_min'] : null,
value_max: isset($p['value_max']) && is_numeric($p['value_max']) ? (float) $p['value_max'] : null,
unit: $p['unit'] ?? null,
symbol: $p['symbol'] ?? null,
group: $p['group'] ?? null,
); );
} }
} }
@ -204,8 +193,8 @@ final class DTOJsonSchemaConverter
$prices[] = new PriceDTO( $prices[] = new PriceDTO(
minimum_discount_amount: (int) ($p['minimum_quantity'] ?? 1), minimum_discount_amount: (int) ($p['minimum_quantity'] ?? 1),
price: (string) ($p['price'] ?? 0), price: (string) ($p['price'] ?? 0),
currency_iso_code: $p['currency'] ?? null, currency_iso_code: $p['currency'] ?? 'USD',
price_related_quantity: 1, price_related_quantity: (int) ($p['minimum_quantity'] ?? 1),
); );
} }
} }
@ -215,7 +204,6 @@ final class DTOJsonSchemaConverter
order_number: $v['order_number'] ?? 'Unknown', order_number: $v['order_number'] ?? 'Unknown',
prices: $prices, prices: $prices,
product_url: $v['product_url'] ?? $productUrl, product_url: $v['product_url'] ?? $productUrl,
prices_include_vat: $v['prices_include_vat'] ?? null,
); );
} }
} }
@ -238,8 +226,7 @@ final class DTOJsonSchemaConverter
manufacturing_status: $manufacturingStatus, manufacturing_status: $manufacturingStatus,
provider_url: $productUrl, provider_url: $productUrl,
footprint: $data['footprint'] ?? null, footprint: $data['footprint'] ?? null,
gtin: $data['gtin'] ?? null, notes: null,
notes: $data['notes'] ?? null,
datasheets: $datasheets, datasheets: $datasheets,
images: $images, images: $images,
parameters: $parameters, parameters: $parameters,

View file

@ -25,24 +25,14 @@ declare(strict_types=1);
namespace App\Services\InfoProviderSystem\Providers; namespace App\Services\InfoProviderSystem\Providers;
use App\Exceptions\ProviderIDNotSupportedException; use App\Exceptions\ProviderIDNotSupportedException;
use App\Helpers\RandomizeUseragentHttpClient;
use App\Services\AI\AIPlatformRegistry; use App\Services\AI\AIPlatformRegistry;
use App\Services\InfoProviderSystem\DTOJsonSchemaConverter; use App\Services\InfoProviderSystem\DTOJsonSchemaConverter;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Settings\InfoProviderSystem\AIExtractorSettings; use App\Settings\InfoProviderSystem\AIExtractorSettings;
use Brick\Schema\SchemaReader;
use Jkphl\Micrometa;
use League\HTMLToMarkdown\HtmlConverter;
use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag; use Symfony\AI\Platform\Message\MessageBag;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient;
use Symfony\Component\Intl\Languages;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
use function Symfony\Component\String\u;
final class AIInfoExtractor implements InfoProviderInterface final class AIInfoExtractor implements InfoProviderInterface
{ {
use FixAndValidateUrlTrait; use FixAndValidateUrlTrait;
@ -57,12 +47,12 @@ final class AIInfoExtractor implements InfoProviderInterface
private readonly AIPlatformRegistry $AIPlatformRegistry, private readonly AIPlatformRegistry $AIPlatformRegistry,
private readonly DTOJsonSchemaConverter $jsonSchemaConverter, private readonly DTOJsonSchemaConverter $jsonSchemaConverter,
) { ) {
//Use NoPrivateNetworkHttpClient to prevent SSRF vulnerabilities, and RandomizeUseragentHttpClient to make it harder for servers to block us $this->httpClient = $httpClient->withOptions([
$this->httpClient = (new RandomizeUseragentHttpClient(new NoPrivateNetworkHttpClient($httpClient)))->withOptions( 'timeout' => 30,
[ 'headers' => [
'timeout' => 15, 'User-Agent' => 'Mozilla/5.0 (compatible; Part-DB AI-Extractor/1.0)',
] ],
); ]);
} }
public function getProviderInfo(): array public function getProviderInfo(): array
@ -105,70 +95,16 @@ final class AIInfoExtractor implements InfoProviderInterface
$html = $response->getContent(); $html = $response->getContent();
// Clean HTML // Clean HTML
/*$cleanedHtml = $this->cleanHTML($html); $cleanedHtml = $this->cleanHTML($html);
// Truncate to max content length // Truncate to max content length
$truncatedHtml = $this->truncateHTML($cleanedHtml, $this->settings->maxContentLength);*/ $truncatedHtml = $this->truncateHTML($cleanedHtml, $this->settings->maxContentLength);
//Convert html to markdown, to provide a cleaner input to the LLM.
$markdown = $this->htmlToMarkdown($html);
//Truncate markdown to max content length, if needed
$markdown = u($markdown)->truncate($this->settings->maxContentLength, '... [truncated]')->toString();
//Extract structured data using traditional methods, to provide additional context to the LLM. This can help improve accuracy, especially for technical specifications that might be in tables or specific formats.
$structuredData = $this->extractStructuredData($html, $url);
// Call LLM // Call LLM
$llmResponse = $this->callLLM($markdown, $url, $structuredData); $llmResponse = $this->callLLM($truncatedHtml, $url);
// Build and return PartDetailDTO // Build and return PartDetailDTO
$result = $this->jsonSchemaConverter->jsonToDTO($llmResponse, $this->getProviderKey(), $url, $url, self::DISTRIBUTOR_NAME); return $this->jsonSchemaConverter->jsonToDTO($llmResponse, $this->getProviderKey(), $url, $url, self::DISTRIBUTOR_NAME);
return $result;
}
/**
* Extracts structured data from the HTML using microformats.
* @param string $html
* @param string $url
* @return string JSON encoded structured data
*/
private function extractStructuredData(string $html, string $url): string
{
$micrometa = new Micrometa\Ports\Parser();
$items = $micrometa($url, $html);
return json_encode($items->toObject(), JSON_THROW_ON_ERROR);
}
private function htmlToMarkdown(string $html): string
{
//Extract only the main content of the page to avoid overwhelming the LLM with irrelevant information.
$crawler = new Crawler($html);
$mainContent = $crawler->filter('main, article, #content');
// If we found a specific content area, get its HTML; otherwise, use the whole body.
//Concat the html of all matched nodes, to provide more context to the LLM, especially for pages that use multiple sections for product info.
if ($mainContent->count() > 0) {
$htmlToConvert = '';
foreach ($mainContent as $node) {
$htmlToConvert .= $node->ownerDocument->saveHTML($node);
$htmlToConvert .= "\n\n"; // Add some spacing between sections
}
} else {
//Use the whole body content, as it might contain relevant information, especially for simpler pages that don't have a clear main/content section.
$htmlToConvert = $html;
}
//Concert to markdown
$converter = new HtmlConverter([
'strip_tags' => true, // Removes tags that aren't Markdown-compatible (like <div>)
'hard_break' => true, // Preserves line breaks
'remove_nodes' => 'nav footer script style' // Extra safety layer
]);
return $converter->convert($htmlToConvert);
} }
public function getCapabilities(): array public function getCapabilities(): array
@ -182,18 +118,55 @@ final class AIInfoExtractor implements InfoProviderInterface
]; ];
} }
private function callLLM(string $htmlContent, string $url, ?string $structuredData = null): array private function cleanHTML(string $html): string
{
// Remove script tags
$html = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $html);
// Remove style tags
$html = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $html);
// Remove nav tags
$html = preg_replace('/<nav\b[^>]*>(.*?)<\/nav>/is', '', $html);
// Remove footer tags
$html = preg_replace('/<footer\b[^>]*>(.*?)<\/footer>/is', '', $html);
// Remove header tags
$html = preg_replace('/<header\b[^>]*>(.*?)<\/header>/is', '', $html);
// Remove HTML comments
$html = preg_replace('/<!--(.*?)-->/is', '', $html);
return $html;
}
private function truncateHTML(string $html, int $maxLength): string
{
if (strlen($html) <= $maxLength) {
return $html;
}
// Truncate and find the last > or space to avoid cutting tags
$truncated = substr($html, 0, $maxLength);
// Find the last occurrence of > or space
$lastPos = max(strrpos($truncated, '>'), strrpos($truncated, ' '));
if ($lastPos !== false && $lastPos > $maxLength * 0.9) {
$truncated = substr($truncated, 0, $lastPos + 1);
}
return $truncated;
}
private function callLLM(string $htmlContent, string $url): array
{ {
$input = new MessageBag( $input = new MessageBag(
Message::forSystem($this->buildSystemPrompt()), Message::forSystem($this->buildSystemPrompt()),
Message::ofUser("Extract part information from this webpage content:\n\nURL: $url\n\n$htmlContent") Message::ofUser("Extract part information from this webpage content:\n\nURL: $url\n\n$htmlContent")
); );
if ($structuredData) {
$input->add(Message::ofUser("Following data was extracted using traditional methods, but might be incomplete or inaccurate.
Enrich it with the actual website data:\n\n".$structuredData));
}
try { try {
$aiPlatform = $this->AIPlatformRegistry->getPlatform($this->settings->platform ?? throw new \RuntimeException('No AI platform selected') ); $aiPlatform = $this->AIPlatformRegistry->getPlatform($this->settings->platform ?? throw new \RuntimeException('No AI platform selected') );
@ -201,7 +174,7 @@ final class AIInfoExtractor implements InfoProviderInterface
$result = $aiPlatform->invoke($this->settings->model ?? throw new \RuntimeException('No model selected'), $input, [ $result = $aiPlatform->invoke($this->settings->model ?? throw new \RuntimeException('No model selected'), $input, [
'response_format' => [ 'response_format' => [
'type' => 'json_schema', 'type' => 'json_schema',
'json_schema' => $this->jsonSchemaConverter->getJSONSchema(), 'json_schema' => $this->jsonSchemaConverter->getJSONSchema(),
] ]
]); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
@ -213,31 +186,41 @@ final class AIInfoExtractor implements InfoProviderInterface
private function buildSystemPrompt(): string private function buildSystemPrompt(): string
{ {
$tmp = <<<'PROMPT' return <<<'PROMPT'
You are an expert at extracting electronic component information from web pages. Extract structured data in JSON format, from markdown extracted from a product page. You are an expert at extracting electronic component information from web pages. Extract structured data in JSON format.
Focus on the main content of the page, such as product descriptions, specifications, and tables. Ignore navigation menus, footers, and sidebars.
Return ONLY a valid JSON object with this exact structure:
{
"name": "string",
"description": "string",
"manufacturer": "string | null",
"mpn": "string | null",
"category": "string | null",
"manufacturing_status": "active|obsolete|nrfnd|discontinued|null",
"footprint": "string | null",
"mass": "number | null (in grams)",
"parameters": [{"name": "string", "value": "string", "unit": "string | null"}],
"datasheets": [{"url": "string", "description": "string"}],
"images": [{"url": "string", "description": "string"}],
"vendor_infos": [{
"distributor_name": "string",
"order_number": "string | null",
"product_url": "string",
"prices": [{"minimum_quantity": int, "price": number, "currency": "string"}]
}],
"manufacturer_product_url": "string | null"
}
Rules: Rules:
- manufacturing_status: Use "active", "obsolete", "nrfnd" (not recommended for new designs), "discontinued", or null - manufacturing_status: Use "active", "obsolete", "nrfnd" (not recommended for new designs), "discontinued", or null
- parameters: Extract technical specs like voltage, current, temperature, etc. and put them into the fields according to the JSON schema. Include units if available. - parameters: Extract technical specs like voltage, current, temperature, etc.
- prices: Extract pricing tiers with minimum_quantity, price, and currency code - prices: Extract pricing tiers with minimum_quantity, price, and currency code
- URLs must be absolute (include https://...) - URLs must be absolute (include https://...)
- If information is not found, use null - If information is not found, use null
- Try to avoid duplicating parameters, if the same parameter is mentioned multiple times, or if it is already used in another field. - Return ONLY the JSON, no explanation text
- Include only the 1 to 3 most relevant images, such as the main product image or important diagrams. Ignore decorative images, logos, or icons.
For parameters, combine name, value, and unit. The unit should be separate if possible.
PROMPT; PROMPT;
if ($this->settings->outputLanguage === null) {
$tmp .= "\n\nProvide the response in the same language of the webpage.";
} else {
$tmp .= "\n\nThe response must be in ". Languages::getName($this->settings->outputLanguage, 'en') ." language. Translate texts if needed.";
}
if ($this->settings->additionalInstructions) {
$tmp .= "\n\nAdditional instructions:\n" . $this->settings->additionalInstructions;
}
return $tmp;
} }
} }

View file

@ -32,10 +32,7 @@ use Jbtronics\SettingsBundle\Settings\Settings;
use Jbtronics\SettingsBundle\Settings\SettingsParameter; use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Jbtronics\SettingsBundle\Settings\SettingsTrait; use Jbtronics\SettingsBundle\Settings\SettingsTrait;
use Symfony\AI\Platform\Capability; use Symfony\AI\Platform\Capability;
use Symfony\Component\Form\Extension\Core\Type\LanguageType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Translation\TranslatableMessage as TM; use Symfony\Component\Translation\TranslatableMessage as TM;
use Symfony\Component\Validator\Constraints\Language;
#[Settings(name: "ai_extractor", label: new TM("settings.ips.ai_extractor"), description: new TM("settings.ips.ai_extractor.description"))] #[Settings(name: "ai_extractor", label: new TM("settings.ips.ai_extractor"), description: new TM("settings.ips.ai_extractor.description"))]
#[SettingsIcon("fa-plug")] #[SettingsIcon("fa-plug")]
@ -59,15 +56,4 @@ class AIExtractorSettings
description: new TM("settings.ips.ai_extractor.max_content_length.description"), description: new TM("settings.ips.ai_extractor.max_content_length.description"),
)] )]
public int $maxContentLength = 50000; public int $maxContentLength = 50000;
#[Language]
#[SettingsParameter(label: new TM("settings.ips.ai_extractor.output_language"), description: new TM("settings.ips.ai_extractor.output_language.description"),
formType: LanguageType::class,
)]
public ?string $outputLanguage = null;
#[SettingsParameter(label: new TM("settings.ips.ai_extractor.additional_instructions"), description: new TM("settings.ips.ai_extractor.additional_instructions.description"),
formType: TextareaType::class,
)]
public ?string $additionalInstructions = null;
} }

View file

@ -40,9 +40,6 @@ class InfoProviderSettings
#[EmbeddedSettings] #[EmbeddedSettings]
public ?GenericWebProviderSettings $genericWebProvider = null; public ?GenericWebProviderSettings $genericWebProvider = null;
#[EmbeddedSettings]
public ?AIExtractorSettings $aiExtractor = null;
#[EmbeddedSettings] #[EmbeddedSettings]
public ?DigikeySettings $digikey = null; public ?DigikeySettings $digikey = null;
@ -79,4 +76,6 @@ class InfoProviderSettings
#[EmbeddedSettings] #[EmbeddedSettings]
public ?CanopySettings $canopy = null; public ?CanopySettings $canopy = null;
#[EmbeddedSettings]
public ?AIExtractorSettings $aiExtractor = null;
} }

View file

@ -441,6 +441,12 @@
"symfony/browser-kit": { "symfony/browser-kit": {
"version": "v4.2.3" "version": "v4.2.3"
}, },
"symfony/cache": {
"version": "v4.2.3"
},
"symfony/cache-contracts": {
"version": "v1.1.5"
},
"symfony/config": { "symfony/config": {
"version": "v4.2.3" "version": "v4.2.3"
}, },

View file

@ -13103,41 +13103,5 @@ Buerklin-API Authentication server:
<target>The AI model that should be used for extraction. Must support structured output.</target> <target>The AI model that should be used for extraction. Must support structured output.</target>
</segment> </segment>
</unit> </unit>
<unit id="H1SYgGs" name="settings.ips.ai_extractor.max_content_length">
<segment>
<source>settings.ips.ai_extractor.max_content_length</source>
<target>Max. Website Content length</target>
</segment>
</unit>
<unit id="SZWiZE3" name="settings.ips.ai_extractor.max_content_length.description">
<segment>
<source>settings.ips.ai_extractor.max_content_length.description</source>
<target>The maximum number of characters of the website that are sent to the AI service.</target>
</segment>
</unit>
<unit id="pCsAHOv" name="settings.ips.ai_extractor.output_language">
<segment>
<source>settings.ips.ai_extractor.output_language</source>
<target>Output language</target>
</segment>
</unit>
<unit id="NVHHgpD" name="settings.ips.ai_extractor.output_language.description">
<segment>
<source>settings.ips.ai_extractor.output_language.description</source>
<target>By default, the providers returns information in the same language as the website. With that option you can ask the AI to translate it for you. Might only work with certain models.</target>
</segment>
</unit>
<unit id="CAeeZlL" name="settings.ips.ai_extractor.additional_instructions">
<segment>
<source>settings.ips.ai_extractor.additional_instructions</source>
<target>Additional instructions</target>
</segment>
</unit>
<unit id=".UaUMk1" name="settings.ips.ai_extractor.additional_instructions.description">
<segment>
<source>settings.ips.ai_extractor.additional_instructions.description</source>
<target>The additional instructions will be appended to the system prompt.</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>