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 — single convert(string $html): string method.
  • Drupal\ai\Service\HtmlToMarkdown\HtmlToMarkdownConverter (default impl) — wraps league/html-to-markdown, options read from new ai.html_to_markdown.settings config, plus an AI-module-only strip_whitespace post-processing step (trims trailing whitespace, collapses blank lines).
  • New config: config/install/ai.html_to_markdown.settings.yml + config/schema/ai.schema.yml entry.
  • New admin form HtmlToMarkdownSettingsForm at /admin/config/ai/html-to-markdown, linked from Configuration.

Optional markdownify integration

  • Drupal\ai\AiServiceProvider (new ServiceModifierInterface): if markdownify.html_converter exists in the container, it swaps ai.html_to_markdown_converter's class/arguments to MarkdownifyHtmlToMarkdownAdapter, which delegates to markdownify.
  • HtmlToMarkdownSettingsController redirects 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 has ai.html_to_markdown.settings configured, it copies those values into markdownify.settings:converters.league so conversion output doesn't change the moment markdownify takes over.
  • composer.json adds drupal/markdownify as a suggest; .gitlab-ci.yml installs it for the composer job so CI exercises this path.

Update path

  • ai_update_15001() installs the default ai.html_to_markdown.settings config 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.php
  • modules/ai_automators/src/PluginBaseClasses/ViewsToText.php
  • modules/ai_content_suggestions/src/Plugin/FieldWidgetAction/PromptContentSuggestion.php
  • modules/ai_search/src/Base/EmbeddingStrategyPluginBase.php
  • modules/ai_search/src/Plugin/AiAssistantAction/RagAction.php

Tests added

  • tests/src/Unit/HtmlToMarkdown/HtmlToMarkdownConverterTest.php
  • tests/src/Unit/HtmlToMarkdown/MarkdownifyHtmlToMarkdownAdapterTest.php
  • tests/src/Unit/AiServiceProviderTest.php
  • tests/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.php

Expected: 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 set

Running 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)

  1. Confirm drupal/markdownify is not enabled.
  2. 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".
  3. Sanity-check conversion output, e.g. via drush php-eval:
    $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);
    Expect a # Title header, **bold**, and [a link](https://example.com).
  4. Exercise a real caller of the service, e.g.:
    • the html_to_markdown AI 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).

4. Markdownify integration

  1. Install drupal/markdownify (composer require drupal/markdownify:^1.1 if not already in the codebase, then drush en markdownify -y).
  2. Confirm settings sync: drush config:get markdownify.settings converters.league should now contain the values that were previously in ai.html_to_markdown.settings (change a value like header_style in the AI form before installing markdownify to make this observable).
  3. Visit /admin/config/ai/html-to-markdown again — 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.
  4. Check the active service implementation:
    print get_class(\Drupal::service('ai.html_to_markdown_converter'));
    // -> Drupal\ai\Service\HtmlToMarkdown\MarkdownifyHtmlToMarkdownAdapter
  5. Re-run the conversion sanity check from step 3.3 — output should be equivalent, now produced by markdownify.
  6. Uninstall markdownify (drush pmu markdownify -y) and confirm the service reverts to Drupal\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

Edited by Artem Dmitriiev

Merge request reports

Loading