The following HTML:
<form action="http://example.com/">
<fieldset class="collapsible collapsed">
<legend>This is the legend</legend>
<div class="collapse-text">
<p>Paragraph text</p>
</div>
</fieldset>
</form>
Produces
<form action="http://example.com/">
<fieldset class="collapsible collapsed"><br />
<legend>This is the legend</legend>
</p>
<div class="collapse-text">
<p>Paragraph text</p>
</div>
<p> </fieldset><br />
</form>
when run through _filter_autop().
If I add 'fieldset' $block in _filter_autop(), I get a much closer to correct result:
<form action="http://example.com/">
<fieldset class="collapsible collapsed">
<legend>This is the legend</legend>
</p>
<div class="collapse-text">
<p>Paragraph text</p>
</div>
</fieldset>
</form>
This issue also affects Drupal 6.
Comments
Comment #1
deviantintegral commentedHere's a first try - I'm having strange exceptions ("SQLSTATE[42S02]: Base table or view not found: 1146 Table 'drupal7.simpletest406869cache_bootstrap' doesn't exist"), so here's a patch before I update and reroll.
Comment #3
deviantintegral commentedHere's an updated patch. It passes for the original issue, but the test exposes a bug with paragraph tags where
<p></p>becomes</p>. To fix the exception, I had to remove the placeholders from my t() calls, so I'm not sure if I'm using it incorrectly or if there is a bug in t().Comment #4
JacobSingh commentedThis is actually bigger than the original title and I'm kinda shocked it has survived so many cycles of Drupal Core.
The linebreak filter is actually from Wordpress (http://ma.tt/scripts/autop/), where it is also broken and people hate it.
Here's an experiment which works on both D6 and D7:
becomes
I'm all for semantically correct HTML, but would prefer using nl2br in this case if we are going to generate invalid XHTML.
Comment #5
Garrett Albright commentedWhat would be the desired output for that, though? GIGO. Are we going to fix the user's input error of stretching a span-level tag across blocks? That'd be a lot of work and might cause trouble with regards to computers trying to be smarter than humans. Perhaps escape the
<strong>tags to make it more apparent something's broken?Comment #6
JacobSingh commentedIt's not the user's input error. The user provided proper HTML. The linebreak filter produces invalid XHTML by trying to wrap the content in
tags and doing it badly.
Comment #7
Garrett Albright commentedBut it is, though, because conceptually, the user is still spreading a span level tag (
<strong>) across two blocks (the<p>s implied by the double line break). I'm not saying this isn't an honest, common mistake; I'm just not sure it's the job of the line-breaks-to-HTML function to fix it. If anything, it's a job for the HTML corrector filter.EDIT: Or are you saying that we simply should not HTML-ize line breaks inside span-level tags?
However, the behavior as demonstrated in the OP's example is definitely wrong.
Comment #8
lotyrin commentedWouldn't the correct behavior be to break up the span level tag?
<strong>Line 1 \n Line2\n\n Line3</strong>becomes
<p><strong>Line1<br/>Line2</strong></p><p><strong>Line 3</strong></p>But I agree, that might not fit in the scope of the line break filter, and might be something to try to catch in HTML corrector. Then again, the actual structural error was introduced by autop, so I'm not sure.
Comment #9
deviantintegral commentedI think any line breaks within span-level tags should always be
<br />. The result should then be:<p><strong>Line 1<br />Line 2<br /><br />Line 3</strong></p>Comment #10
Garrett Albright commentedLooking at this chunk of code further… It looks like we're setting ourselves up for failure from the outset by trying to define all block-level tags. I notice this patch adds a couple, but there's still a bunch still missing, particularly new HTML 5 ones -
<canvas>,<article>,<header>and<footer>, etc. Is there some way to determine if a tag is block-level without a list like this…? Or am I in fantasy-land?Does anyone know how other text filters handle this? I'm going to take a peek at what PHP Markdown is doing…
Comment #11
deviantintegral commentedOriginally, there were two issues opened. One, to fix the current filter, second to rewrite the entire thing. I think at this point we should give up trying to fix the existing code, and move over to #659580: Specify what the line break converter should do and rewrite it in DOM.
That's fine for D7 and beyond, but D6 still supports PHP4, so perhaps our best bet for fixing D6 is to test for whatever PHP5 features we need and use a DOM parser if possible (since most D6 sites should be on PHP5), and revert to the old parser for PHP4.
Comment #12
Garrett Albright commentedI don't like that idea. It would leave things open too much for inconsistent behavior depending on which version of PHP is running.
Comment #13
Garrett Albright commentedThinking over this, I thought the situation might be better if we stripped out whitespace between HTML tags, so that
_filter_autop()wouldn't have any line breaks to get tripped up on. So I ran this through:Despite any wrapping your browser may be doing, there's no actual line breaks in there. But here was the output:
What the snot?! How is this function finding line breaks that don't exist?! Why is it inserting its own?! This thing has got to go!
Comment #14
JacobSingh commentedI agree, and I'm in favor of HTMLPurifier for the HTML fixing and for the XSS security.
-J
Comment #15
carlos8f commentedI noticed that the previous patches added
<legend>as block-level, and it is in fact inline and shouldn't be added.I also found a couple regexp's that use the $block snippet without including "
[^>]*" at the end, so any whitespace or attributes at the end of a block-level tag cause unexpected results. That's why there's a useless<br />after<legend>in #13, since the next tag is a<div>with a class.There are obviously more problems than just with non-block elements. I did a quick test with HEAD:
The output:
... so wrong!
Comment #16
carlos8f commentedActually,
<legend>should probably be added, since structurally it is never used in the flow of text, even if it is technically non block-level. Adding<legend>to $block does fix the stray<p>after the</div>. However I do agree that it could get ridiculous adding tags to these hard-coded lists as the bugs crop up. Looking at WordPress's version, they have added quite a few tags to the list since we borrowed the code.WordPress's current code has the same bugs...but they probably run a balance tags function to correct the output, which would explain a lot :) In Drupal at least, the line break filter should probably be self-contained and not depend on the HTML corrector filter. People expect "faulty and chopped off HTML" to be user-entered, not resulting from a series of crappy regexp's :P
I'll experiment with some document parsing too, although I realize D8 has an initiative for that in #659580: Specify what the line break converter should do and rewrite it in DOM.
Comment #17
carlos8f commentedHere's a new version of the filter that should avoid some of the egregious tag balancing errors. There are undoubtedly bugs with this code though, so testing is welcome.
Comment #19
andypost.
Comment #20
hgurol commented#17: filter-autop.2.patch queued for re-testing.
Comment #22
carlos8f commentedThis version keeps track of a tag stack, which is used to determine what type of tag we're inside of.
</p><p>is avoided inside inline tags and<br /><br />is substituted. Also did some bug fixes and tests. My version of the filter is more complex than the old one, but adds the ability to insert logic into the process.It's kind of a work in progress right now, and needs more testing, but so far I think it produces the expected results from the test snippets posted. Example:
Input:
Output:
Comment #23
carlos8f commentedMore bug fixes and unit tests. The tests include running the filter through some complex HTML examples, and the new filter is holding up pretty well.
The new tests use a new method, FilterUnitTestCase->assertTagOrder(), which is useful for making sure new tags are added in the right places and nesting is correct. It allows the filter to have some freedom to change the whitespace in the HTML a bit, as long as the tag order is good.
Comment #24
carlos8f commentedHere's a comparison of the new vs. old autop filter when the HTML contains extra spacing:
Input:
HEAD autop:
patched autop:
:)
Comment #25
naheemsays commentedis using
<br /><br />correct?I think we can presume that the user intended a paragraph break. More, the default drupal texareas do not add (AFAIK)
<p></p>tags to the code and double line breaks are used unless the person has configured a wysiwyg textarea along with the necessary changes to the input format.I don't think it is a good idea to have
<br /><br />as the output of what is the default situation.Comment #26
carlos8f commentedFair enough, instead of
<br /><br />we now close the inline tags in the current tag stack and start a new paragraph:Input:
Output:
Re: the second statement, let's be clear that the changes I'm making to this filter don't result in
<p>being added to any textareas, since Drupal doesn't run filtered output back through forms. Further, the filter is also built to respect and not mess with<p></p>that a user or wysiwyg editor may enter into a textarea. So the changes here don't affect anything other than fixing the completely broken markup that the current filter produces.Comment #27
carlos8f commentedMade a correction to the tag stack logic and tweaked the test to check if we close the inline tags before closing
<p>.Comment #28
carlos8f commentedComment #29
aspilicious commentedCan you fill in the doc blocks please. (add @param and @return blocks)
Comment #30
carlos8f commentedAlso to clarify: we were only using
<br /><br />in the limited situation when an inline tag was open and we didn't want to create invalid XHTML by ending the paragraph in the middle of that. This behavior was suggested in comment #9. The new patch's behavior is equivalent to #8, which as you say is perhaps more what the user intended.Comment #31
Garrett Albright commentedI think using
<br /><br />in span-level tags instead of closing and re-opening it around a paragraph break is simpler and more predictable behavior, and less prone to causing problems. It may not technically be "correct," but it will still result in output that the user, who is most likely a novice if they're making this mistake, is expecting. +1 for going back to that approach.Comment #32
naheemsays commented-1 to going back to that approach unless we find specific problems - drupal does not ship with a wysiwyg. If someone enters tags manually or using a tag editor like bueditor/markitup etc, by default there will be no paragraph markup. A person can easily highlight a section of content to bold or make italic (strong/em) and expect the markup that comes out at the end to be good.
If we can fix any problems that arise, I think doing it properly is worth it.
Saying that, here are my findings:
If the line break filter is before the htmlcorrector: (default)
- strong, em and span apply to the first two paragraphs only and not to subsequent ones.
- del only applies to one paragraph.
If the line break filter is after the htmlcorrector:
- strong, em and del work properly (tested with 4 paragraphs only though) and each paragraph has the correct breaking and opening.
- span however only applies to the first two.
Comment #33
JacobSingh commented@nbz:
There are specific problems. See my earlier comments. It's absolutely reproducible and very easy to do on a stock install. Just add a double linebreak inside of a <strong> or <code> tag. Produces bad XHTML.
We got the autop script from wordpress. autop is broken there too.
http://7and1.net/2009/08/17/how-can-i-disable-wps-fucking-autop-filter/
http://urbangiraffe.com/plugins/disable-wpautop/
On my own WP site I did a test:
http://pajamadesign.com/2010/05/26/test-silly/
See the source there, it produces invalid XHTML.
We should certainly forgo semantic "correctness" for syntax correctness until the problem is properly solved (I vote for HTMLPurifier).
There is a real bug here, and it has nothing to do with the HTML corrector.
-J
Comment #34
sunI do not see any critical functionality being broken here.
These global variables need to go.
Why is legend not in the block level tags list, but added here?
I think these should be variables.
78 critical left. Go review some!
Comment #35
carlos8f commented- Converted globals to drupal_static().
- Got rid of block/inline taglist functions, side-effect being that $blocklevel is now hardcoded in two different functions. Added
<legend>to $blocklevel since we don't want it wrapped in<p></p>.- Went back to
<br /><br />when inside inline tags. With the...</strong></p><p><strong>...approach, it gets difficult to maintain that across multiple paragraphs, and ensure that nothing breaks between those paragraphs. Also the problem when you might have<strong id="some-id">and then if you re-create the strong you would have to strip out the id attribute.<br /><br />is not as pretty but should avoid all those types of complications.- Added doxygen.
Comment #36
JacobSingh commentedWow! Really impressive effort here. The tests are much better, and the behavior is a lot more sane! Ditto to Sun's comment about legend kinda hanging out there by itself. Is there a reason for that?
Best,
Jacob
Comment #37
naheemsays commented"del" is not listed in inline tags and any attributes for the span tag seem to be stripped by the line break filter (I tried <span style="color:blue"> but the parsed results gave <span>)
Comment #38
naheemsays commentedSetting to CNW
More, the correctness of the output using <br /><br /> seems to so far be the same as using <p></p> so I am not totally convinced by that argument.
Saying that, any improvement is better than none.
Comment #39
carlos8f commentedAdded del to inline tag list, and could not reproduce the attribute stripping bug. Added a quick test to assert that attributes are preserved. nbz, could you post the raw text input you used for which attributes were stripped?
Comment #40
naheemsays commentedWhat I am using to test:
The last one there strips the style statement from the tag. I am also seeing different results in node teaser (in this case the front page for that text), but I am not sure if that is a cache issue or something else (I ahve been clearing the cache after making any changes.)
Comment #41
carlos8f commentedI ran your example through _filter_autop() and the output looked good. The attribute stripping you're seeing must be from a different filter, probably "Limit allowed HTML tags". That filter strips all style attributes, I assume for XXS-prevention. Try using Full HTML text format, or run the text directly through _filter_autop() to make sure that no other factors are interfering. This issue strictly addresses the _filter_autop() function and all we're trying to do is make it output correct XHTML. I use a test.php in the Drupal root to do testing:
Comment #42
JacobSingh commentedIndeed, style attributes get stripped.
This is a big problem for WYSIWYG editors. The DrupalGardens team is working on porting HTMLPurifier to DRUPAL-7 to solve this problem.
-J
Comment #43
lotyrin commented#39: filter-autop.8.patch queued for re-testing.
Comment #44
carlos8f commentedQuick re-roll without the new tests. The autop tests in HEAD have changed, so let's see how the patch fairs.
Comment #45
sunThis is a totally nice effort. However, we absolutely have to start with writing tests. Leveraging the new FilterUnitTestCase::assertFilteredString() method. That is, because as of now, we have close to zero expectations for the line break filter.
So what I'd like to see in the next patch here is no filter.module changes (for now) and only new test assertions in filter.test's testLineBreakFilter(), which prove that the our expectations (whatever they are) are not met. The new assertions should follow the coding style of the existing in testLineBreakFilter() as well as testUrlFilter(). Extra points for finding a way to categorize assertions in some way, like testUrlFilter() does.
I already attempted to do this myself, but I had no clue at all what our expectations for this filter actually are. Therefore, this is the main, primary, and most important challenge to solve, before doing anything else.
After adding expectations, we can add back the filter.module hunks, if necessary, or fix the bugs in another way, whatever makes sense. But please understand that I'm not able to review or even RTBC a patch that changes the behavior of one of Drupal core's default input filters without adding unit tests.
Comment #46
sunAlso, please don't take that "or fix the bugs in another way" as an insult... you hard work is highly appreciated! I merely want to point out that our copy/fork of WP's autop is outdated: #900956: Update autop filter to include missing block level tags -- so that may be another possibility to try to fix the bugs.
Comment #47
carlos8f commentedThe expectation is pretty basic: post valid XHTML in your node content, and the line break filter should not produce invalid XHTML out of that. I wrote tests for this (see the last few hunks of #39) which check the tag order of the output, if there are stray
</p>etc, the tests will fail. The tests are very specific in the tag order though, so there's some improvement to be done (maybe simply checking that no invalid nesting is going on, AND that we have at least some paragraph tags that weren't already there, rather than a specific tag order). I didn't include the tests in #44 because the HEAD tests have changed, so I want to test against those and I need to re-roll my tests.By the way, the current version of the WP filter is just as broken, so I don't think upgrading will fix anything. The problem is that in WP, they use an HTML corrector every time to clean up the preg_replace() mess (there are lots of stray tags left over), while we leave HTML corrector as a separate filter and therefore the line break filter is not sufficient in itself.
I'll work on the tests and post that separately so we can see the problem clearer.
Comment #48
carlos8f commentedHere's a manual test, which needs to be converted into a SimpleTest:
The result is usually broken XHTML -- failures attached.
Comment #49
carlos8f commentedHere are some tests written around the assumption that we can use _filter_htmlcorrector() to act as our XHTML validator, implemented as assertCorrectHTML(). If _filter_htmlcorrector() returns different content, we have reason to believe we gave it invalid XHTML.
Note that test 2 fails, but test 3 passes. The only difference is an added "pre" tag, which is curious. Somehow the "pre" tag screws up the processing of content below it.
Apply patch #44 and the assertCorrectHTML()'s do pass, but other tests fail because they are overly specific to WP autop. WP autop totally changes the whitespace of the original content, adding newlines as it pleases, while my autop tries to keep the original whitespace. WP autop also strips out empty paragraph tag pairs, which IMO is changing the input unnecessarily so I also avoid that. The current tests need to be rewritten to be more flexible and not check for exact output. Having a whitespace-insensitive equality check would be a start, and from there we can decide on the behavior, i.e. should the filter strip out empty p tag pairs, and should the filter add paragraphs inside block level tags.
Comment #50
carlos8f commentedI found that the HTML corrector actually does change the whitespace in some cases, which could cause assertCorrectHTML() to fail. I now added assertIdenticalWithoutWhitespace(), which should adjust for that. Also now asserting that our source is valid HTML, before we run it through a filter.
Comment #51
sunhumm... why didn't you go with the existing assertFilteredString() method? PHP's DOM implementation can have bugs, too. I'd highly prefer to see regular input/output assertions, just like all the other revised LineBreakFilter/UrlFilter tests.
Comment #52
carlos8f commentedAt first I tried assertFilteredString() but it didn't seem to suit the situation since I'm not checking for exact output, just that the output is valid HTML. The code would be like:
Even then, the assertion doesn't work because the comparison needs to ignore whitespace, since (I discovered) _filter_htmlcorrector() may change the whitespace of already valid HTML. assertCorrectHTML() is the alternative I came up with.
Comment #53
sunThat's exactly what I mean. These are unit tests, so HTML corrector should not be involved at all. It adds entirely different expectations to the Line break filter's expectations, which invalidates the entire test. We only want to test _filter_autop(), and nothing else. Therefore, we want and have to use simple input/output assertions.
Comment #54
carlos8f commentedMy idea was to make the tests flexible in terms of whitespace, since the current autop filter adds gratuitous whitespace as a side-effect of its sloppy preg_replace() mechanism. I'd like to transition to my implementation, which preserves whitespace. To transition though we would need to rewrite the tests to expect different whitespace, if we use simple input/output assertions. We can certainly do that, I just think (ideally) the assertions shouldn't be so specific about whitespace, which in XHTML doesn't matter a whole lot, but tag nesting/order certainly matters.
Comment #55
carlos8f commentedReturning to #45, I think we just need to document all the gory details of what this filter currently does, and what it ideally should do (with respect to tags added/removed and whitespace added/removed). Only then can we write "pure" input/output tests. I was hoping to get by by just adding the expectation "output should be valid XHTML, given valid XHTML as input" and fixing that aspect. I guess that's not really going to cut it :)
Comment #56
sun.core commentedTrying to make sense of the major queue currently. This might be an annoying bug, but doesn't affect the majority of users, and can also be circumvented in many ways. Demoting to normal.
Comment #57
Anonymous (not verified) commentedsubscribe.
Comment #58
mcurry commentedsubscribe
Comment #59
quicksketchWell while this issue got totally stuck, the front-end team managed to fix most HTML5 tags in #1280522: _filter_autop (line break converter) does not recognize html5 block level elements. Figures that's what you get when you don't over-think things. :P
I've rerolled a patch similar to deviantintegral's original patches that adds remaining missing tags over in #900956: Update autop filter to include missing block level tags.
Comment #60
crystalidea commentedThe only working solution we found is using HTML Purifier module (to fix broken HTML) afterwards
Comment #61
David_Rothstein commentedPossibly related: #1938746: _filter_autop inserts </p> without <p>