Use service for html to markdown convertion + markdownify integration
Summary
Introduces a central ai.html_to_markdown_converter service so the whole ai
project shares one HTML→Markdown implementation instead of each caller
instantiating its own league/html-to-markdown HtmlConverter, and makes
that implementation swappable in favor of the optional drupal/markdownify
module.
New service & interface
Drupal\ai\Service\HtmlToMarkdown\HtmlToMarkdownConverterInterface— singleconvert(string $html): stringmethod.Drupal\ai\Service\HtmlToMarkdown\HtmlToMarkdownConverter(default impl) — wrapsleague/html-to-markdown, options read from newai.html_to_markdown.settingsconfig, plus an AI-module-onlystrip_whitespacepost-processing step (trims trailing whitespace, collapses blank lines).- New config:
config/install/ai.html_to_markdown.settings.yml+config/schema/ai.schema.ymlentry. - New admin form
HtmlToMarkdownSettingsFormat/admin/config/ai/html-to-markdown, linked from Configuration.
Optional markdownify integration
Drupal\ai\AiServiceProvider(newServiceModifierInterface): ifmarkdownify.html_converterexists in the container, it swapsai.html_to_markdown_converter's class/arguments toMarkdownifyHtmlToMarkdownAdapter, which delegates to markdownify.HtmlToMarkdownSettingsControllerredirects the AI settings page to markdownify's own settings page when markdownify is installed (the AI form is inert at that point).ai_modules_installed()gained_ai_sync_markdownify_league_settings(): when markdownify is installed on a site that already hasai.html_to_markdown.settingsconfigured, it copies those values intomarkdownify.settings:converters.leagueso conversion output doesn't change the moment markdownify takes over.composer.jsonaddsdrupal/markdownifyas asuggest;.gitlab-ci.ymlinstalls it for the composer job so CI exercises this path.
Update path
ai_update_15001()installs the defaultai.html_to_markdown.settingsconfig on existing sites (no-ops if the config already exists).
Callers migrated to the new service (previously each created its own HtmlConverter, with slightly inconsistent option sets):
src/Plugin/AiFunctionCall/HtmlToMarkdown.phpmodules/ai_automators/src/PluginBaseClasses/ViewsToText.phpmodules/ai_content_suggestions/src/Plugin/FieldWidgetAction/PromptContentSuggestion.phpmodules/ai_search/src/Base/EmbeddingStrategyPluginBase.phpmodules/ai_search/src/Plugin/AiAssistantAction/RagAction.php
Tests added
tests/src/Unit/HtmlToMarkdown/HtmlToMarkdownConverterTest.phptests/src/Unit/HtmlToMarkdown/MarkdownifyHtmlToMarkdownAdapterTest.phptests/src/Unit/AiServiceProviderTest.phptests/src/Kernel/MarkdownifyInstallSyncTest.php(skips gracefully if markdownify isn't present in the codebase)
Testing instructions
1. Automated tests
# Unit tests (converter, adapter, service provider swap)
vendor/bin/phpunit -c web/core/phpunit.xml.dist --group ai \
web/modules/contrib/ai/tests/src/Unit/HtmlToMarkdown \
web/modules/contrib/ai/tests/src/Unit/AiServiceProviderTest.php
# Kernel test (requires drupal/markdownify present in the codebase; skips otherwise)
vendor/bin/phpunit -c web/core/phpunit.xml.dist --group ai \
web/modules/contrib/ai/tests/src/Kernel/MarkdownifyInstallSyncTest.phpExpected: all green (verified locally: 12 unit assertions + 2 kernel tests, no failures — only pre-existing core deprecation noise unrelated to this change).
2. Update hook on an existing site
On a site where ai was already installed before this patch (so ai.html_to_markdown.settings doesn't exist yet):
drush config:get ai.html_to_markdown.settings # -> "Config ... does not exist"
drush updb -y # runs ai_update_15001
drush config:get ai.html_to_markdown.settings # -> shows the full default option setRunning drush updb -y again should not error and should not reset any values you've since changed (the hook is a no-op once the config exists).
3. Default converter (markdownify NOT installed)
- Confirm
drupal/markdownifyis not enabled. - Visit
/admin/config/ai/html-to-markdown— the AI settings form loads (not a redirect), showing all league/html-to-markdown options plus "Strip whitespace". - Sanity-check conversion output, e.g. via
drush php-eval:Expect a$html = '<h1>Title</h1><p>Some <strong>bold</strong> text and <a href="https://example.com">a link</a>.</p>'; print \Drupal::service('ai.html_to_markdown_converter')->convert($html);# Titleheader,**bold**, and[a link](https://example.com). - Exercise a real caller of the service, e.g.:
- the
html_to_markdownAI function/tool call, - an AI Automator field using a "Views to text" rule with "Convert HTML to markdown" enabled,
- AI Content Suggestions' field widget action on a content type,
- AI Search's RAG assistant action / an embedding strategy that chunks rendered entity output.
These should behave exactly as before (same default options:
strip_tags,strip_placeholder_links, table conversion enabled).
- the
4. Markdownify integration
- Install
drupal/markdownify(composer require drupal/markdownify:^1.1if not already in the codebase, thendrush en markdownify -y). - Confirm settings sync:
drush config:get markdownify.settings converters.leagueshould now contain the values that were previously inai.html_to_markdown.settings(change a value likeheader_stylein the AI form before installing markdownify to make this observable). - Visit
/admin/config/ai/html-to-markdownagain — it should now redirect (HTTP 200 after redirect, final URL/admin/config/services/markdownify) to markdownify's own settings form instead of showing the AI form. - Check the active service implementation:
print get_class(\Drupal::service('ai.html_to_markdown_converter')); // -> Drupal\ai\Service\HtmlToMarkdown\MarkdownifyHtmlToMarkdownAdapter - Re-run the conversion sanity check from step 3.3 — output should be equivalent, now produced by markdownify.
- Uninstall markdownify (
drush pmu markdownify -y) and confirm the service reverts toDrupal\ai\Service\HtmlToMarkdown\HtmlToMarkdownConverter.
5. CI
Verify the composer job in .gitlab-ci.yml installs drupal/markdownify:^1.1, so the kernel test in step 1 actually runs (rather than being skipped) in the pipeline.
All of the above was run and verified locally against this branch (ddev, PHP 8.3, Drupal 11) prior to posting.
Checklist
- I have linked the related issue in the MR title or description
- I have performed a self-review of my own code
- I have added or updated tests, or explained in the description why this change is not covered by tests
- I have updated documentation for any new or changed functionality
- I have written testing instructions and verified them locally
- I have noted any required post-merge steps (config imports, cache rebuilds, manual changes)
- This MR contains no breaking API or hook changes, or they are explicitly documented in the description
AI Compliance
Note
Check the one that best describes your usage, or leave all unchecked if AI was not significantly used.
-
AI Assisted Code
Mainly written by a human; AI used for autocomplete or partial generation under full human supervision. -
AI Generated Code
Mainly generated by AI, reviewed and approved by a human before this MR was created. -
Vibe Coded
Generated by AI and only functionally reviewed before this MR was created.
Closes #3574246