Example 1 has similarities to example 4. I'll come back to this...
Examples 2 and 3 are the basic cases. In example 2, ignoring the code
tags, you have the pre
start tag, a new line character that creates the blank line, "foo", a new line character that ends that line and then immediately the pre
end tag. Note that a single space before the end tag is sufficient to create a blank line under the "foo" line.
Example 3 contains no new lines at all. But pre
is, by default, a block level element so "foo" appears on a line of its own anyway. Nothing more to say about this.
Example 4 is like example 2 except that the code tags have moved lines. This exposes a particular oddity of the HTML parser. The HTML parsing algorithm says:
↪ A start tag whose tag name is one of: "pre", "listing"
If the stack of open elements has a p element in button scope, then close a p element.
Insert an HTML element for the token.
If the next token is a U+000A LINE FEED (LF) character token, then ignore that token and move on to the next one. (Newlines at the start
of pre blocks are ignored as an authoring convenience.)
Set the frameset-ok flag to "not ok".
So the initial new line character, which immediate follows the pre
start tag, gets dropped on the floor and there is no blank line before the "foo" line. Whereas in Example 2 the code
start tag is the token which immediately follows the pre
start tag, causing the new line character which follows that to not be ignored.
Example 1 is likewise. The initial new line following the pre
start tag is ignored, the new line following the 'codestart tag creates a blank line, then the foo line and its new line character, then the new line following the 'code
end tag creates a blank line below the "foo" line, and them the pre
element ends.