Tutorials 29 min read

How to Draw a Directory Tree in Markdown

MMNMNOTE
markdowndirectory-treecode-blockstree-commandreference
Updated November 3, 2026

Wrap the tree in a fenced code block. Generate it with tree, which prints box-drawing characters under a UTF-8 locale and plain |-- with --charset=ascii. Across ten renderer modes, the fence kept every byte in nine, whichever characters the tree used. Without a fence, all ten damaged it 1.

proj
├── README.md
├── docs
│   ├── guides
│   │   └── setup.md
│   └── index.md
└── src
    ├── main.py
    └── pkg
        ├── __init__.py
        └── util_a.py

That is real output from tree 2.3.1 on a throwaway folder, unedited 2. People keep looking for it. Two Stack Overflow threads on the question hold 864,024 views between them as of 31 October 2026 3. "Print Directory & File Structure with icons for representation in Markdown" has 494,292 — a score of 435. "Is there a way to represent a directory tree in a Github README.md?" has 369,732 — a score of 188.

The usual debate is which characters are safest. The measurement dissolves it — at the parser, the character set decides nothing. The fence decides. What differs afterwards is font, encoding and the reader.

One limit, stated early. Renderer results come from six engines in ten modes. Pixel results come from Linux with redistributable fonts — your font will differ. Obsidian, the VS Code app, Windows, macOS, any PDF path and any screen reader were not run. The method section lists the exact boundary.


The block to copy, and the command that makes it

Run tree on the folder, paste the output between two lines of three backticks, and leave a blank line above and below. The command here keeps two levels, lists folders first and skips node_modules. It also drops the closing file count — four flags that trim a real project to something a reader will actually look at.

tree -L 2 --dirsfirst --noreport -I node_modules proj
proj
├── docs
│   ├── guides
│   └── index.md
├── src
│   ├── pkg
│   └── main.py
└── README.md

In the note itself, the source looks like this:

The layout:

```
proj
├── docs
│   ├── guides
│   └── index.md
├── src
│   ├── pkg
│   └── main.py
└── README.md
```

The next paragraph starts here.

The fence works because the parser stops reading. CommonMark says so in one line: "The content of a code fence is treated as literal text, not parsed as inlines." 4 Nothing inside is emphasis, nothing is a list, and every space stays where it was.

Five fenced cases went through ten modes — box drawing, ASCII, emoji, +--, and box drawing labelled text. Each stayed byte-exact in nine 1. The miss is Python-Markdown with default extensions — it has no fenced blocks, and wraps the tree in an inline <code> inside a paragraph. Add its fenced_code extension and the bytes come through.

The blank lines are for you. GitHub's documentation puts it modestly: "We recommend placing a blank line before and after code blocks to make the raw formatting easier to read." 5 Both code-block forms sit in the complete Markdown cheat sheet.

Fence the tree. Most of what follows is about what happens after that.


Which characters to use: the quick reference

Inside a fence, the choice of characters changes nothing at the parser — box drawing, ASCII, +-- and emoji all stayed byte-exact in nine of ten modes. The choice decides the later costs: font coverage, line height, encoding, and what a reader hears. Default to box drawing unless the file may meet a tool that does not read UTF-8.

ConventionInside a fenceBare pasteBytes, 11-line treeMain cost after the parserPick it when
Box drawing (tree)9 of 10 exactdamaged, 10 of 10239gaps at wide leading; missing from 3 of 12 fonts; garbles if read as Windows-1252default for a UTF-8 README or note
ASCII (tree --charset=ascii)9 of 10 exactdamaged, 10 of 10; backticks pair up163rougher look; no connected line at any leadingthe file may meet a non-UTF-8 tool, or a printing path you cannot test
+--, typed by hand9 of 10 exactdamaged, 10 of 10—you maintain it by handno tool is available
Emoji with heavy box (📂, ┣)9 of 10 exactdamaged, 10 of 10; bold varies—hollow box with no emoji font; small shift after each one; a descriptor on every linedecorative use, for sighted readers

Pick by what comes after the parser.

The ASCII form comes from one flag. The tree man page describes it: "Set the character set to use when outputting HTML and for line drawing." 6

tree --charset=ascii --noreport -I node_modules proj
proj
|-- README.md
|-- docs
|   |-- guides
|   |   `-- setup.md
|   `-- index.md
`-- src
    |-- main.py
    `-- pkg
        |-- __init__.py
        `-- util_a.py

Under a UTF-8 locale, --charset=utf-8 produces the same bytes as plain tree — skip it. Under LC_ALL=C, plain tree already gives the ASCII form above 2.

The ASCII form has a catch of its own — a backtick on every last-child line, five of them in these eleven lines. Inside a fence they are harmless. Outside one they turn into code delimiters — the same counting problem covered in how to put a backtick inside code in Markdown.

The hand-typed version needs no tool at all. The accepted answer on the 2013 thread offers it this way: "If you are concerned about Unicode characters you can use ASCII to build the structures" 7.

proj
+-- README.md
+-- docs
|   +-- guides
|   |   +-- setup.md
|   +-- index.md
+-- src
    +-- main.py
    +-- pkg
        +-- __init__.py
        +-- util_a.py

The emoji version is the shape from a Stack Overflow answer with a score of 191 — the sample below is typed for this test, not copied 8.

📦proj
 ┣ 📂docs
 ┃ ┣ 📂guides
 ┃ ┃ ┗ 📜setup.md
 ┃ ┗ 📜index.md
 ┣ 📂src
 ┃ ┣ 📂pkg
 ┃ ┃ ┣ 📜__init__.py
 ┃ ┃ ┗ 📜util_a.py
 ┃ ┗ 📜main.py
 ┗ 📜README.md

What a bare paste does to a tree

Paste a tree with no fence and it stops being a tree. In all ten modes, __init__.py became <strong>init</strong>.py. In eight, the leading spaces were gone from the output. The other two left them inside a paragraph, where a browser collapses them. Only one mode kept the line breaks as line breaks 9.

This is the HTML GitHub's file-rendering mode returned for the first tree on this page:

<p>proj
├── README.md
├── docs
│   ├── guides
│   │   └── setup.md
│   └── index.md
└── src
├── main.py
└── pkg
├── <strong>init</strong>.py
└── util_a.py</p>

Read the bottom half — main.py no longer sits under src, and the double underscores became bold. The reason the indent disappears is covered in Markdown throws away your extra spaces, so it is not re-taught here. A file name without a double underscore loses the bold. It does not get the indent back.

Lines fare no better — nine of the ten modes left the tree as ordinary text inside one <p>, with no <br>, and a browser flows that together. GitHub's comment mode is the exception — it adds a <br> to each line 9.

The ASCII form fails more strangely, because its backticks pair up across lines into code spans. GitHub's file mode again:

<p>proj
|-- README.md
|-- docs
|   |-- guides
|   |   <code>-- setup.md |   </code>-- index.md
<code>-- src |-- main.py </code>-- pkg
|-- <strong>init</strong>.py
`-- util_a.py</p>

That pairing happened in ten of ten modes 9. Pandoc's markdown reader adds one more change, turning each -- outside a code span into an en dash.

The emoji form is where bold depends on the engine. __init__ went bold in six modes and stayed plain in four — commonmark.js, micromark and both GitHub modes — because engines disagree about an underscore that follows an emoji. The leading space still went, and so did the line structure everywhere but GitHub's comment mode 9.

None of this raises an error. A renderer has no way to know a tree was meant.


The tree commands worth keeping

Four commands handle most jobs: -L for depth, --gitignore to hide what git ignores, -d for folders only, and /f /a on Windows. Every Linux command below ran on tree 2.3.1; older versions may lack a flag 10. The Windows one is taken from Microsoft's documentation and was not run.

Hide what git ignores. The flag reads the .gitignore files it finds, so node_modules left the test folder's output without being named:

tree --gitignore --noreport proj

Folders only, for a top-level map of a large project:

tree -d --noreport -I node_modules proj
proj
├── docs
│   └── guides
└── src
    └── pkg

On Windows, the built-in command takes different switches. Microsoft documents /a as "Specifies to use text characters instead of graphic characters to show the lines that link subdirectories." and /f as "Displays the names of the files in each directory." 11

tree /f /a

What that command writes into a file on a given code page was not tested, so this page makes no claim about redirecting it.

Two flags do not belong in a note. -S writes CP437 bytes — ├── reads as ��� under UTF-8. -A writes terminal escape sequences into the file 12.

The invisible character in tree output

In UTF-8 mode, tree indents with no-break spaces. The eleven-line tree above holds eight U+00A0 characters beside its 38 ordinary spaces — the guides line runs │, no-break space, no-break space, space, ├ 2. All eight survived GitHub's renderer in each case checked 1.

It renders fine. The surprise comes when you search or edit: a search for │ ├ typed with ordinary spaces finds nothing in that output. To normalize a pasted tree with GNU sed, pipe it through this — the search then matched in the test:

tree --noreport -I node_modules proj | sed 's/\xC2\xA0/ /g'

The ASCII form has no no-break spaces at all 2.


When a fence is the wrong container: four other forms

Use a fence by default and switch for a reason. An indented block works where fenced blocks are unavailable. A <pre> block can hold links but depends on raw HTML. A nested list gives real structure and links but loses the drawn shape. A script can generate that list for you.

A fence cannot hold a link.

GitHub returned [README.md](README.md) inside a fence as those literal characters 13. That one fact drives most of the alternatives.

Four-space indent, after a blank line. This is the one form that stayed byte-exact in ten of ten modes — Python-Markdown's miss does not apply 1. The blank line is not optional: directly under a paragraph, the same indent collapsed in all ten 9. Obsidian's help documents the form — "You can also create a code block by indenting the text using Tab or 4 blank spaces." 14 That sentence documents syntax. It is not a rendering test.

Layout:

    proj
    └── README.md

A label on the fence. A text info string keeps the same bytes; GitHub's file mode adds only lang="text" to the <pre> 1. Use it if your renderer would otherwise guess at highlighting.

A <pre> block. Links can live inside, and on GitHub an <a> inside <pre> stayed a link 13. Eight of ten modes kept the bytes. The other two — markdown-it and micromark, both at default settings with raw HTML off — printed the tag as text 1.

<pre>
├── <a href="README.md">README.md</a>
</pre>

A nested list with code names. This is the only form with list structure of its own, and it can carry links. Backticks protect the names: with them, __init__.py stayed intact in all ten modes; without them, it went bold in all ten 9. Indent four spaces per level.

With two spaces, GitHub's two modes and the CommonMark-family engines nested the list correctly, but Python-Markdown flattened it. With four spaces, all ten modes put every name at the right depth 15.

- `proj/`
    - `README.md`
    - `docs/`
        - `guides/`
            - `setup.md`
        - `index.md`
    - `src/`
        - `main.py`
        - `pkg/`
            - `__init__.py`
            - `util_a.py`

A generated linked list. A script by Simon on Stack Overflow, with a score of 81 and licensed CC BY-SA 3.0, turns tree output into that list 16. The version here is adapted: it names node_modules as the exclusion and wraps each name in backticks, because the original leaves __init__.py unprotected in the link text. It needs GNU sed, and -t orders entries by modification time.

tree -tf --noreport -I node_modules --charset ascii proj |
  sed -e 's/| \+/  /g' -e 's/[|`]-\+/ */g' \
      -e 's:\(* \)\(\(.*/\)\([^/]\+\)\):\1[`\4`](\2):g'
proj

 * [`README.md`](proj/README.md)
 * [`docs`](proj/docs)
   * [`guides`](proj/docs/guides)
     * [`setup.md`](proj/docs/guides/setup.md)
   * [`index.md`](proj/docs/index.md)
 * [`src`](proj/src)
     * [`main.py`](proj/src/main.py)
     * [`pkg`](proj/src/pkg)
         * [`__init__.py`](proj/src/pkg/__init__.py)
         * [`util_a.py`](proj/src/pkg/util_a.py)

Add a blank line after the first line, as shown — the script does not print one. Without it, Python-Markdown and pandoc's markdown reader printed the lines as a paragraph rather than a list. Even with it, Python-Markdown misplaced part of the hierarchy 15. The other modes, GitHub's two included, nested it as drawn.


Why the vertical line has gaps on GitHub

The gaps come from line height against glyph height — your Markdown has nothing to do with them. GitHub sets line-height: 1.45 on pre, and most fonts draw │ shorter than that. The line breaks by a few pixels at each row. You cannot change GitHub's leading. You can change the leading and font in your own notes.

The original Stack Overflow asker wanted exactly this: "some hint for the best syntax to include the output in a Markdown document, without spaces between lines." 3 No syntax delivers it.

Measured in Chromium 156 on Linux, using each project's real stylesheet, the break per row was 17:

FontGitHub's stylesheet (1.45)VS Code's stylesheet (1.357em)
Liberation Mono5px3.5px
DejaVu Sans Mono4px2.5px
JetBrains Mono2px0.5px
Fira Code00

The font files tell the same story without a browser. Of the nine fonts that carry the glyphs, │ falls short of the line in eight at 1.45, six at 1.357 and three at 1.2 18. Fira Code's glyph is 1.5em tall — which is why it joins up. The browser rounded glyph widths to whole pixels here, but ink height in the font file does not depend on that.

Read the table as a shape, not as what you will see. GitHub's stack asks for SF Mono, Menlo and Consolas before Liberation Mono 19. Those three are not redistributable, and were not measured — a Mac or Windows reader on GitHub gets something else. The VS Code column used its stylesheet with an editor font set for the test — the app itself was never opened 20.

It is cosmetic. Nothing is lost but the join.

In your own notes, leading is yours to set, and the leading inside a code block can run tighter than prose. Tighter leading closes the gap — so does a font with a taller glyph.


What box drawing costs: fonts, width, encoding

Box drawing is the right default — and it still has three conditional costs. Some monospace fonts lack the glyphs. The characters have an ambiguous width class, so East Asian contexts can draw them wide. And the UTF-8 bytes garble when something reads them as Windows-1252. Each one is usually cosmetic — the bare-paste damage is neither cosmetic nor conditional.

The four characters have longer official names than you might guess 21:

GlyphCode pointUnicode name
├U+251CBOX DRAWINGS LIGHT VERTICAL AND RIGHT
└U+2514BOX DRAWINGS LIGHT UP AND RIGHT
│U+2502BOX DRAWINGS LIGHT VERTICAL
─U+2500BOX DRAWINGS LIGHT HORIZONTAL

They are older than they look. Unicode's own note on the block says: "All of these characters are intended for compatibility with character cell graphic sets in use prior to 1990." 22

Fonts. Nine of twelve monospace fonts measured carry all four, each at the same advance as M. The current Google Fonts files of Roboto Mono, Space Mono and Courier Prime carry none 18. A browser falls back to another font for them, and in the Linux test that fallback glyph was 8.188px wide against a 9px cell in Space Mono 23.

Width. The whole range is East Asian Ambiguous: 2500..254B ; A in Unicode's data file 24. UAX #11 explains the term: "Ambiguous characters occur in East Asian legacy character sets as wide characters, but as narrow (i.e., normal-width) characters in non–East Asian usage." 25

A terminal configured for wide ambiguous characters can therefore stretch the connectors. The same report sets the default in a separate section: "If the context cannot be established reliably, they should be treated as narrow characters by default." 25 Most readers never see the wide case, and it was not reproduced in this test.

Encoding. Read as Windows-1252, ├── becomes ├── and │ becomes │ 26. The bytes are intact, and the reader chose the wrong table — the mechanism is in what plain text actually means.

Text diagrams earn their reputation for lasting, with one qualification. The bytes last. How they paint depends on the font. For a diagram drawn by hand rather than a folder listed by a command, a grid editor is the better tool — the same font caveat applies to whatever it produces.


Emoji trees, fairly

Emoji do not break the connector columns — they sit after the connectors. They shift only the text that follows them, by a pixel or three on Linux. Their real costs are a hollow box where no emoji font exists — and a descriptor on every line for anyone listening. For a decorative tree, that may be fine.

Unicode's width data classes these characters as Wide — 1F442..1F4FC ; W covers 📁 📂 📄 📜 📦 24. The emoji report says they "will typically have about the same vertical placement and advance width as CJK ideographs." 27 Two cells, in other words.

The test came close to that. In Chromium on Linux, at 13.6px in Liberation Mono with Noto Color Emoji, 📁 measured 17px against two cells of 16px — so the text after it moved one pixel. In Ubuntu Mono it moved three 23.

Other emoji fonts will differ.

Take the emoji font away and the picture changes — the glyph drops to a hollow one-cell box, and text after it moves eight pixels left. wcswidth still reports two cells 23. None of the twelve monospace fonts measured carries 📁 itself, so the emoji always comes from somewhere else 18.

Hearing them is a different question — one this test did not run. CLDR's text-to-speech names are file folder, open file folder and scroll 28. Tigwell, Gorman and Menzies explain the link in a CHI 2020 paper: "These descriptors are then used by screen readers to vocalise what the emoji visually represents." 29 Among the disadvantages their survey participants named was "repeated auditory feedback when multiple emoji are used (6)" 29.

That study is about emoji in social messaging, so treat it as an analogy. A spoken file folder may well be more useful to a listener than a line character.


Screen readers: write one sentence beside the tree

A drawn tree carries its hierarchy in formatting alone, and the standards ask for a sentence alongside. No source here tested a screen reader on a Markdown directory tree, and neither did this test. So the advice stays modest — say in words what the tree shows, or use a nested list when structure matters.

The HTML standard addresses preformatted text directly: "Authors are encouraged to consider how preformatted text will be experienced when the formatting is lost, as will be the case for users of speech synthesizers, braille displays, and the like. For cases like ASCII art, it is likely that an alternative presentation, such as a textual description, would be more universally accessible to the readers of the document." 30

W3C's technique H86 is shorter: "If ASCII art is used, it should also have a text explanation of what the picture is." 31 The page's own header reads "Examples of ways to meet WCAG; not required" — and a directory tree is a borderline case of ASCII art.

The closest research is about terminals, not Markdown pages. Sampath, Merrick and Macvean studied 12 developers using command-line tools with screen readers, at CHI 2021. Writing about progress indicators, they say: "Screen readers do not read ASCII-art or non-ASCII characters as intended for sighted developers." 32 One participant was blunter: "ASCII Art is beyond annoying, and it is very common" 32. That is an analogy from a terminal — useful as a warning, weak as a measurement.

The sentence costs almost nothing:

The project has two top-level folders: `docs` holds the guides and `src` holds the code.

```
proj
├── docs
└── src
```

How the test was run

One real eleven-line tree output, with one awkward file name, went through fourteen cases in six engines and ten modes on 31 October 2026. Font files were read directly, and pixel gaps came from headless Chromium on Linux with redistributable fonts. Anything beyond that boundary — apps, other systems, assistive technology — was not run, and is listed below.

The ten modes, exactly 1:

GitHub's renderer is a live service, so it is the one result that could drift. All 28 GitHub calls were re-run an hour later the same day, and every output matched byte for byte.

The __init__.py name was chosen because it is a common real trigger. The fence results follow from the spec rather than from that name — but this remains one tree.

Not run:


FAQ

Is there a way to represent a directory tree in a Github README.md?

There is no dedicated syntax. Put tree output in a fenced code block. The fence kept every byte in both GitHub modes tested, and in nine of ten modes overall. Without it, the tree loses its indent and __init__.py turns bold.

Use tree for the structure. Emoji cost a hollow box where no emoji font exists, a small shift in the text after each one, and a descriptor on every line for anyone using a screen reader. Keep them inside a fence.

How to automatically create README.md markdown of directory tree listing

Run tree -L 2 --dirsfirst --noreport -I node_modules proj and paste the result into a fence. For a list of links rather than a drawing, use the sed script in the section on other forms. Re-run either one when the layout changes.

Use a nested list — a fence cannot hold a link. The generated-list script above turns tree output into * [name](path) lines. Wrap the names in backticks so a file like __init__.py stays intact in the link text.

On GitHub, an <a> inside <pre> stayed a link in the test. Two of ten modes — markdown-it and micromark at their default settings — printed the <pre> tag as text, because raw HTML was off. Check your renderer before relying on it.

Why are there spaces between the lines of my tree in Markdown?

It is line height against glyph height, not your Markdown. GitHub's pre uses 1.45, and most fonts draw │ shorter than that. You cannot change GitHub's. In your own notes, tighten the code-block leading or use a font with a taller glyph, such as Fira Code.


A tree is text until a parser reads it. Fence it, and the parser leaves it alone.


Folder layouts and the sentences that explain them, kept as open Markdown notes on your own device — mnmnote.com.

Footnotes

  1. Own measurement, 2026-10-31. One 11-line tree 2.3.1 output in 14 cases, each rendered as its own document. GitHub REST POST /markdown in gfm and markdown modes; commonmark.js 0.31.2; markdown-it 15.0.3; micromark 4.0.3 with micromark-extension-gfm 3.0.0; pandoc 3.13 with markdown, gfm and commonmark readers; Python-Markdown 3.11.2, default and with fenced_code. Five fenced cases: byte-exact in 9 of 10 modes. Indented after a blank line: 10 of 10. <pre>: 8 of 10. Byte-exact means the code text matched the input apart from the final newline, which the three pandoc modes drop. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  2. Own measurement, 2026-10-31, tree v2.3.1, © 1996–2026 by Steve Baker, Thomas Moore, Francesc Rocher, Florian Sesser, Kyosuke Tokoro. UTF-8 output: 239 bytes, 8 U+00A0, 38 U+0020. --charset=ascii: 163 bytes, 0 U+00A0, 5 backticks. --charset=utf-8 and plain tree under UTF-8 byte-identical; --charset=ascii and plain tree under LC_ALL=C byte-identical. ↩ ↩2 ↩3 ↩4

  3. Stack Overflow, via the Stack Exchange API. q/19699059, https://stackoverflow.com/questions/19699059 (asked 2013-10-31; 494,292 views, score 435), and q/23989232, https://stackoverflow.com/questions/23989232 (asked 2014-06-02; 369,732 views, score 188). Figures as of 2026-10-31. ↩ ↩2

  4. John MacFarlane, CommonMark Spec, version 0.31.2 (2024-01-28), §4.5 "Fenced code blocks." https://spec.commonmark.org/0.31.2/#fenced-code-blocks — accessed 2026-10-31. ↩

  5. GitHub Docs, "Creating and highlighting code blocks." https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks — accessed 2026-10-31. ↩

  6. tree man page, doc/tree.1, --charset. https://gitlab.com/OldManProgrammer/unix-tree/-/raw/master/doc/tree.1 — accessed 2026-10-31. ↩

  7. RobertKenny, answer to q/19699059, Stack Overflow, CC BY-SA 3.0. https://stackoverflow.com/a/19702570 — accessed 2026-10-31. ↩

  8. Will Ward, answer to q/19699059, Stack Overflow, CC BY-SA 4.0, score 191 on 2026-10-31. https://stackoverflow.com/a/57086391 — accessed 2026-10-31. ↩

  9. Own measurement, 2026-10-31, same modes. Bare box, ASCII and +-- trees: __init__.py bold in 10 of 10. Plain leading spaces removed from the output in 8 of 10. Line breaks kept as <br> only in GitHub's gfm mode. Bare ASCII: backticks paired into code spans in 10 of 10. Bare emoji: bold in 6 of 10. Indent directly under a paragraph: collapsed in 10 of 10. Nested list with code names: names intact in 10 of 10; bare names: bold in 10 of 10. ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  10. Own measurement, 2026-10-31, tree 2.3.1 on a throwaway folder containing a .gitignore that lists node_modules/. ↩

  11. Microsoft Learn, "tree" command reference. https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/tree — accessed 2026-10-31. ↩

  12. Own measurement, 2026-10-31, tree 2.3.1. -S wrote bytes c3 c4 c4 for the first connector. -A wrote \x1b(0tqq\x1b(B before the first file name. ↩

  13. Own measurement, GitHub REST POST /markdown, markdown and gfm modes, 2026-10-31. Fence input returned [README.md](README.md) as code text; <a> inside <pre> returned as a link. ↩ ↩2

  14. Obsidian Help, "Basic formatting syntax." https://raw.githubusercontent.com/obsidianmd/obsidian-help/master/en/Editing%20and%20formatting/Basic%20formatting%20syntax.md — accessed 2026-10-31. ↩

  15. Own measurement, 2026-10-31, depth of each file name in the rendered list. Two-space and four-space nested lists, and the generated linked list with its blank line, in all ten modes; the linked list without the blank line in the eight local modes. ↩ ↩2

  16. Simon, answer to q/23989232, Stack Overflow, CC BY-SA 3.0, score 81 on 2026-10-31. https://stackoverflow.com/a/35889620 — accessed 2026-10-31. The sed expressions shown here are adapted from that script. ↩

  17. Own measurement, 2026-10-31, headless Chromium 156 on Linux, device scale 2. Fonts loaded from file. GitHub rows at 13.6px, VS Code rows at 14px. ↩

  18. Own measurement, 2026-10-31, cmap, advance and U+2502 ink height read from 12 font files: DejaVu Sans Mono 2.37, Fira Code 5.002, IBM Plex Mono 2.3, Inconsolata 3.001, JetBrains Mono 2.211, Liberation Mono 2.1.5, Noto Sans Mono 2.014, Source Code Pro 1.026, Ubuntu Mono 0.80, Roboto Mono 3.001, Space Mono 1.003, Courier Prime 3.018. ↩ ↩2 ↩3

  19. github-markdown-css, --fontStack-monospace and .markdown-body pre. https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown.css — accessed 2026-10-31. ↩

  20. Microsoft, Visual Studio Code, extensions/markdown-language-features/media/markdown.css. https://raw.githubusercontent.com/microsoft/vscode/main/extensions/markdown-language-features/media/markdown.css — accessed 2026-10-31. ↩

  21. Unicode Consortium, UnicodeData.txt, Unicode 18.0.0. https://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt — accessed 2026-10-31. ↩

  22. Unicode Consortium, NamesList-18.0.0, Box Drawing block header. https://www.unicode.org/Public/UCD/latest/ucd/NamesList.txt — accessed 2026-10-31. ↩

  23. Own measurement, 2026-10-31, headless Chromium 156 on Linux at 13.6px. Emoji font: Noto Color Emoji, and a second run with no emoji font. wcswidth from wcwidth 0.9.4. Linux only. ↩ ↩2 ↩3

  24. Unicode Consortium, EastAsianWidth-18.0.0.txt (2026-06-29). https://www.unicode.org/Public/UCD/latest/ucd/EastAsianWidth.txt — accessed 2026-10-31. ↩ ↩2

  25. Ken Lunde (ed.), Unicode Standard Annex #11, "East Asian Width," revision 46 (2026-07-31). https://www.unicode.org/reports/tr11/ — accessed 2026-10-31. ↩ ↩2

  26. Own measurement, 2026-10-31, UTF-8 bytes of the connectors decoded as Windows-1252 with Python's codec. ↩

  27. Unicode Technical Standard #51, "Unicode Emoji," version 18.0 (2026-08-28). https://www.unicode.org/reports/tr51/ — accessed 2026-10-31. ↩

  28. Unicode CLDR, common/annotations/en.xml, type="tts" entries. https://raw.githubusercontent.com/unicode-org/cldr/main/common/annotations/en.xml — accessed 2026-10-31. ↩

  29. Garreth W. Tigwell, Benjamin M. Gorman, Rachel Menzies, "Emoji Accessibility for Visually Impaired People," CHI 2020. https://doi.org/10.1145/3313831.3376267 — author copy: http://eprints.bournemouth.ac.uk/33854/1/tigwell-gorman-menzies-CHI2020.pdf — accessed 2026-10-31. ↩ ↩2

  30. WHATWG, HTML Living Standard, §4.4.3 "The pre element." https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element — accessed 2026-10-31. ↩

  31. W3C WAI, WCAG 2.2 Technique H86, "Providing text alternatives for emojis, emoticons, ASCII art, and leetspeak." https://www.w3.org/WAI/WCAG22/Techniques/html/H86 — accessed 2026-10-31. ↩

  32. Harini Sampath, Alice Merrick, Andrew Macvean, "Accessibility of Command Line Interfaces," CHI 2021, Recommendation 6 and §5.4. https://doi.org/10.1145/3411764.3445544 — accessed 2026-10-31. ↩ ↩2

  33. Microsoft, Visual Studio Code, extensions/markdown-language-features/package.json. https://raw.githubusercontent.com/microsoft/vscode/main/extensions/markdown-language-features/package.json — accessed 2026-10-31. ↩