Where the wall of literal text came from
While tidying an internal document I dropped a ready-made piece of HTML into the Markdown: one <div class="callout"> wrapping three lines of caution, so its border would flag the passage as 这段配置别乱改 . After conversion the border was gone, the tags lay in a tidy row on the floor, and every angle bracket had become < and >.
The blocker was the switch named 允许 HTML, Allow HTML, off by default: while it is off a <b> in the body is always escaped into literal text, and only with it on do inline and block-level tags pass through untouched. The same source line x <b>y</b> z comes out 28 characters with the switch off and 20 characters with it on, and the gap is exactly the markup that escaping puffed outward.
Turning it on brought a new puzzle: the <div> in my example code block was still literal, as if nothing had happened. So here is the passthrough edge line by line, plus the places where tables, task lists and heading levels get their blame pointed at the wrong rule.
Three hard boundaries, one miss and you are back to escaping
The first boundary is that an entity has to arrive whole. My source held ©, &, A and A; with the switch closed every one of them is escaped a second time as a whole, leaving © one layer too many in front of the name. Open the switch and named, decimal and hexadecimal entities that are complete are let through together. A lone ampersand and a bare quotation mark are not on that list: even switched on they are still written as & and ".
The second boundary is the shape of the tag. Passthrough wants a letter or an exclamation mark right after the less-than sign and a greater-than sign to close it. A comment in the middle of a paragraph, a <!-- hidden --> b, is escaped even switched on, because the inline branch takes no comments. A comment on a line of its own, <!-- c -->, lands verbatim, since the character class in the block-level branch carries the exclamation mark. An unclosed <b c is ordinary text as well.
The third boundary is the length of the attribute body. At exactly 400 characters <b x="…"> passes through whole. Add one more to reach 401 and the opening tag is escaped while the closing tag </b> still passes, so one line of HTML is split half into escaped text and half into markup.
Those are the only two ceilings I could reach: the attribute body stops at 400 characters, the notice list at 20 entries, with anything from the 21st onward dropped in silence. Nothing judges input length or volume, and there is no fallback for text that runs long.
The code block path only ever visits escaping
Fenced blocks and four-space indented blocks both go straight into the escaping step and are never handed to the branch that processes raw HTML. A <div class="x">y</div> inside a ```html fence therefore yields byte-identical output whether the switch is up or down, and a script pasted into indented code comes out as escaped text under both settings.
Inline code reads the same way: a <b> inside a pair of backticks is emitted as <code><b></code> on both sides of the switch. With Allow HTML on, the whole entities inside those backticks get escaped one layer deeper, landing somewhere more convoluted than with it off.
It stops feeling odd once phrased this way: the switch answers whether the markup I typed is genuine HTML, while a code block announces that it shows somebody else's HTML. I believed it only after running one source through Markdown to HTML under both settings and comparing outputs character by character.
Two things that get worse once it is on
The first casualty is the angle-bracket autolink. A bare <https://example.org> occupying a line of its own gives a proper anchor while the switch is off. Turn it on and the block-level branch matches first, so the output stays the literal <https://example.org> and the link is gone. Put a few words in front of it and the line falls back onto the inline branch, where the anchor reappears.
The second is that this switch is not a filter. There is no tag whitelist behind it: with it on, <script>alert(1)</script> enters the result untouched, the event attribute on <span onclick="alert(1)"> is not washed away, and <iframe> is copied as written, while the preview side uses innerHTML, so those tags really take effect. Its position is one thing only: I know what I am writing, and nothing about trusting what I paste.
Tables, task lists and headings: more silence than notices
Table handling needs one divider row carrying both a hyphen and a pipe. Miss it by a little and the landing place changes completely:
| Input | Measured output |
|---|---|
| Header, divider row and one data row | 206 characters, thead and tbody both complete |
| The same table with GFM turned off | The whole table retreats into one <p> and no table tag appears |
| The divider row deleted | Also retreats into <p>, with no notice at all |
| A data row with one cell more than the header | Only 2 <td> come out, the surplus vanishes, and the notice reads 表格第 1 行单元格比表头多 |
Three columns written as :---, :---:, ---: | Only centre and right alignment receive a style; the left column carries none |
| a | with a line of --- right beneath it | Read as a setext underline, so the table becomes <h2 id="a"> |
- The row number in that notice is the index of a data row, not a line number in your file, so counting back into the original text with it lands in the wrong place.
- Pressing convert a second time overwrites the result in full; the notices and bodies of two runs are never stacked together.
- Task items written as
- [x]and- [ ]emit<input type="checkbox" checked disabled>, each<li>arrives withclass="md-task", and an uppercase X counts as checked likewise. With GFM off the bracket marker stays in the body as written, no class and no input element.
Two heading facts get blamed on the wrong rule. A line opening with seven hash signs is not a heading at all, the deepest level is h6, so the contents block nests strictly by level and a skipped level inserts an empty <ul> layer. Heading ids come from a hand-rolled slug: ASCII punctuation discarded whole, Chinese kept, spaces turned into hyphens, repeats given -1, -2 in turn. None of that comes from CommonMark or GFM.
An id is written whenever either heading anchors or generated contents is true. Turn anchors off with contents still on and the body shows <h1 id="a">; only with both off do you receive tags carrying no id.
Three questions you cannot route around
Q: Can HTML inside a code block ever come out as real markup?
It cannot. The fenced branch and the indented branch only visit escaping and never consult Allow HTML. Want real structure, write it into the body and open the switch. Want the code itself on display, keep it in the block, since literal text is exactly what you asked for.
Q: With Allow HTML on, does a pasted script get blocked?
Nothing blocks it: this is no security filter and there is no whitelist of tags. A script tag, a span with an event attribute and an iframe all enter the result as written, and the preview really acts on them. For anything going to the public web I run the document once with the switch closed and treat the literals as a review list.
Q: Why do the character figure and the HTML figure disagree?
They use different rulers. The character column counts UTF-16 code units of the source you typed, the HTML column counts UTF-8 bytes of the output. Mix in Chinese and the distance widens: one Chinese character is a single code unit yet clearly more than one byte.
Passthrough covers only tags in the body proper that arrive complete and short enough. Code blocks, entities that never form a whole and half-written angle brackets fall outside it. Knowing which branch your passage takes tells you more than memorising the switch name.