Problem/Motivation
The default 'Summary or trimmed' formatter specifically includes HTML tags in the character count, which is not very intuitive. The trim count should exclude HTML tags, which Smart Trim module does.
This is explicitly tested for in https://git.drupalcode.org/project/drupal/-/blob/HEAD/core/modules/text/...
Steps to reproduce
- Install Drupal using the Standard install
- Create a content type with a body text
- Modify the article teaser display to trim at 100 characters
- Create an article with the following markup as the body:
<p><a href="https://wwww.drupal.org/"><em><span><strong>The maximum number of characters used in the trimmed version of a post. Drupal will use this setting to determine at which offset long posts should be trimmed Note that this setting will only affect new or updated content and will not affect existing teasers.</strong></span></em></a></p>- which 344 characters long - See that the text "The maximum number of characters used in the" is displayed which is less then 100 - because the HTML was counted.
Proposed resolution
Add a new setting to the trimmed formatters to exclude HTML
Remaining tasks
MR with tests- Review
User interface changes

Introduced terminology
N/A
API changes
New setting added to trimmed formatters
Data model changes
New key in field.formatter.settings.text_trimmed / text_summary_or_trimmed config schema plus upgrade path
Release notes snippet
NA
| Comment | File | Size | Author |
|---|---|---|---|
| #18 | Screenshot 2026-07-31 at 9.38.30 AM.png | 83.98 KB | smustgrave |
Issue fork drupal-3473241
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
- 3473241-strip-html
changes, plain diff MR !16499
- 3473241-strip-tags-from
changes, plain diff MR !9469
Comments
Comment #2
pameeela commentedSplit off from #221257: text_summary() should output valid HTML and Unicode text
Comment #5
annmarysruthy commentedComment #6
annmarysruthy commentedComment #8
sriharsha.uppuluri commentedFixed the test case failure issue
Comment #9
smustgrave commentedAppears to be missing test cases and upgrade path.
Comment #10
grevil commentedJust ran into this while using metatag + glossify.
Glossify provides filters, which automaticall create tooltips / links for words, for which a node or a taxonomy term exists. These links use the linked node body, as the tooltip, which results in a VERY long "title" HTML Tag:

Now, if I would like to use the token [node:summary] inside a metatag or anywhere else, the summary won't be displayed correctly as you can see here:

Now this stems from the same issue, this issue is about. But the current MR doesn't solve it deeply enough. The real root cause of this is the
method from the core "text" module.
In my opinion we should strip the tags there. The method doc says the following:
I don't think stripping HTML tags would change the documented functionality of this method.
Comment #11
anybodyThanks, I can also confirm this issue and this is definitely a bug, not a feature request. The expected behaviour is to strip the affected text.
For example, using the Metatag module (which uses [node:summary] by default for node meta descriptions!), this bug will break the metatag generation if the shortened text has a long link in the beginning. The text will be stripped right before the link and that's it.
Comment #13
shmy commentedRun into that while working on Text trimmed by "Trimmed" or "Summary or trimmed" field formatters will get run through Xss::filter() potentially changing their content. We may want to merge / or close one of these issues.
Comment #14
anybody@shmy: They're definitely related, but I don't see them as duplicate?
Still both should be solved (and maybe solved together).
Comment #15
shmy commentedBut their MR overlap as they eventually try solve the same underlying issue (partly in the same way).
Comment #16
smustgrave commentedI actually agreed with @shmy that they were duplicates. So lets continue here. Working on the open tags.
Comment #18
smustgrave commentedComment #19
smustgrave commentedComment #20
smustgrave commentedReady for some eyes
Comment #21
dcam commentedI found a couple of minor things that I put in the MR comments. The trimming works for me. I actually run into this problem yesterday while testing other Text module issues!
Comment #22
smustgrave commentedApplied 1 suggestion and answered another. Thanks for the review @dcam!
Comment #23
dcam commentedI just realized that the update function sets the settings to TRUE. That will cause the output to change for existing fields, which could cause problems. I think that it's appropriate for the setting to default to TRUE for new fields, but existing fields need to have it set to FALSE.
Comment #24
smustgrave commented100% right. I’ll update
Comment #25
smustgrave commentedComment #26
dcam commentedThe FunctionalJavascript tests aren't executing properly on my local environment. And I can't re-execute them on GitLab. Do we know if they're random?
Comment #27
smustgrave commentedNot sure why it would fail when updating the update hook to be false but ran a new pipeline and still failed so will look into it
Comment #28
smustgrave commentedAppears to be random https://www.drupal.org/project/drupal/issues/3614445#comment-16715712
Comment #29
marcoscanoDid some code review and slightly adjusted the IS.
I agree with the general approach, but I feel like the hand-parsing of
:: truncateHtml()as implemented is fragile (see issues found in the MR) and will end up being complex to maintain over time.How about we switch the approach there, and cut the raw HTML at the byte offset where we've already detected the visible limit lies, and (accepting that the fragment contains incorrectly-closed HTML), just let core's
HTML::normalize()repair it?Something along the lines of:
Should we have a Change Record for this? There is a new setting + new behavior + new public method, etc.
Also, I manually tested this on this issue's Tugboat, and I'm seeing the text example from the Issue Summary get truncated at character 71 instead of 100. Am I testing this wrong or the test coverage is not picking the scenario of the example?
Comment #30
smustgrave commentedDefinitely will need a change record I believe
Comment #31
smustgrave commentedDisclosure I did use AI to help with the feedback but reviewed the changes. Manually testing still but thoughts?
Comment #32
marcoscanoAdded a few comments to the review, and re-worded the Change Record to be more comprehensive.
AI disclosure: I have used an LLM to help analyze this issue, whose output I reviewed and filtered.
Comment #33
smustgrave commentedThink ready for eyes again.
Comment #34
marcoscanoI think there is one minor issue with the test, but apart from that I can't find anything else! 👍
Comment #35
meeni_dhobale commentedTested MR !16499. While testing found two cases where
truncateHtml()withexclude_html_tagsbreaks.First one. If the body has an HTML comment with a > in it before the real close, the cutoff can land inside it. Something like
<!-- note: a > b -->or an old style<!--[if IE]>...<![endif]-->block. Then the teaser comes out empty. Tested with trim_length 20, both cases gave<p></p>, no text at all.Second one. If the body has a
<script>or<style>block, the cutoff can land inside it too. Then the raw code shows up as visible text on the page instead of getting dropped. Tested with trim_length 10, got<p>Hi</p>if (a > b) {for script, and<p>Hi</p>.x { confor style.Both come from the same thing.
truncateHtml()flips its tag state on every > character, so it does not know about comments or script/style blocks. A > inside either one ends that state too early.Checked this on a real page load too, not just direct calls.
Comment #36
macsim commentedI noticed a few months ago (as far as I remember it was on a drupal 10) that something was wrong with the count when input contains
\r\n: it should count as 1 but it counted as 2.I'm not sure whether it was due to custom code or something from core, but it's worth keeping that in mind and checking this here too.
Comment #37
shmy commentedThanks for continuing here @smustgrave.
I'm not convinced that a custom parser is the best approach here. It may become a continues maintenance burden to handle the next edge case and eventually providing a dedicated HTML parser or declaring that only a subset is supported. Some where already found by @meeni_dhobale, but there more. E.g.:
- a open/close tag might appear in the content itself (e.g. a
<code>tag while doc on truncateHtml() declare HTML input the value might not have passed `htmlspecialchars()` and therefore might not have been decoded to`</>`; does the method needs to be public? afaik a private method would to the job as well)- a closing tag may be present (escaped by
'or") in a any attribute (e.g.data-something="who-knowns-why->"or anonClickattribute might contain`() => do something`(not sure why someone would pass that into a field, but there is nothing that prevents them from doing that when filters are disabled))Im wondering what/if there is an the actual usecase for including the HTML tags lengh when calculating the length? In my option its just counterintuitive, but i might not have met that requirement yet. Im just worried we are committing to support a case that is very rarely, if ever, used.
Without the need to track the tag length it might be a lot easier process and manipulate DOMText values as HTML::normalize will eventually anyway hand over the content to that parser.
Comment #38
shmy commentedI guess the use-case for the length count is for BC. Did forget about that.
I feel sorry for disrupting the current approach.
And im not sure if, in the end, the mentioned DOM processing would more or less replicate TruncateHtml of smart trim module and/or there is a middle ground.
I've also did see the explicitly tested broken HTML output (mentioned in the summary) in TextSummaryTest but didn't had any time to check why that has been the accepted/intended behavior in the first place.