mdream 1.6.0

Fastest HTML-to-Markdown converter. Zero dependencies, streaming support.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
// Constants converted from const.ts

pub const TAG_HTML: u8 = 0;
pub const TAG_HEAD: u8 = 1;
pub const TAG_DETAILS: u8 = 2;
pub const TAG_SUMMARY: u8 = 3;
pub const TAG_TITLE: u8 = 4;
pub const TAG_META: u8 = 5;
pub const TAG_BR: u8 = 6;
pub const TAG_H1: u8 = 7;
pub const TAG_H2: u8 = 8;
pub const TAG_H3: u8 = 9;
pub const TAG_H4: u8 = 10;
pub const TAG_H5: u8 = 11;
pub const TAG_H6: u8 = 12;
pub const TAG_HR: u8 = 13;
pub const TAG_STRONG: u8 = 14;
pub const TAG_B: u8 = 15;
pub const TAG_EM: u8 = 16;
pub const TAG_I: u8 = 17;
pub const TAG_DEL: u8 = 18;
pub const TAG_SUB: u8 = 19;
pub const TAG_SUP: u8 = 20;
pub const TAG_INS: u8 = 21;
pub const TAG_BLOCKQUOTE: u8 = 22;
pub const TAG_CODE: u8 = 23;
pub const TAG_UL: u8 = 24;
pub const TAG_LI: u8 = 25;
pub const TAG_A: u8 = 26;
pub const TAG_IMG: u8 = 27;
pub const TAG_TABLE: u8 = 28;
pub const TAG_THEAD: u8 = 29;
pub const TAG_TR: u8 = 30;
pub const TAG_TH: u8 = 31;
pub const TAG_TD: u8 = 32;
pub const TAG_OL: u8 = 33;
pub const TAG_PRE: u8 = 34;
pub const TAG_P: u8 = 35;
pub const TAG_DIV: u8 = 36;
pub const TAG_SPAN: u8 = 37;
pub const TAG_TBODY: u8 = 38;
pub const TAG_TFOOT: u8 = 39;
pub const TAG_FORM: u8 = 40;
pub const TAG_NAV: u8 = 41;
pub const TAG_LABEL: u8 = 42;
pub const TAG_BUTTON: u8 = 43;
pub const TAG_BODY: u8 = 44;
pub const TAG_CENTER: u8 = 45;
pub const TAG_KBD: u8 = 46;
pub const TAG_FOOTER: u8 = 47;
pub const TAG_PATH: u8 = 48;
pub const TAG_SVG: u8 = 49;
pub const TAG_ARTICLE: u8 = 50;
pub const TAG_SECTION: u8 = 51;
pub const TAG_SCRIPT: u8 = 52;
pub const TAG_STYLE: u8 = 53;
pub const TAG_LINK: u8 = 54;
pub const TAG_AREA: u8 = 55;
pub const TAG_BASE: u8 = 56;
pub const TAG_COL: u8 = 57;
pub const TAG_EMBED: u8 = 58;
pub const TAG_INPUT: u8 = 59;
pub const TAG_KEYGEN: u8 = 60;
pub const TAG_PARAM: u8 = 61;
pub const TAG_SOURCE: u8 = 62;
pub const TAG_TRACK: u8 = 63;
pub const TAG_WBR: u8 = 64;
pub const TAG_SELECT: u8 = 65;
pub const TAG_TEXTAREA: u8 = 66;
pub const TAG_OPTION: u8 = 67;
pub const TAG_FIELDSET: u8 = 68;
pub const TAG_LEGEND: u8 = 69;
pub const TAG_AUDIO: u8 = 70;
pub const TAG_VIDEO: u8 = 71;
pub const TAG_CANVAS: u8 = 72;
pub const TAG_IFRAME: u8 = 73;
pub const TAG_MAP: u8 = 74;
pub const TAG_DIALOG: u8 = 75;
pub const TAG_METER: u8 = 76;
pub const TAG_PROGRESS: u8 = 77;
pub const TAG_TEMPLATE: u8 = 78;
pub const TAG_ABBR: u8 = 79;
pub const TAG_MARK: u8 = 80;
pub const TAG_Q: u8 = 81;
pub const TAG_SAMP: u8 = 82;
pub const TAG_SMALL: u8 = 83;
pub const TAG_NOSCRIPT: u8 = 84;
pub const TAG_NOFRAMES: u8 = 85;
pub const TAG_XMP: u8 = 86;
pub const TAG_PLAINTEXT: u8 = 87;
pub const TAG_ASIDE: u8 = 88;
pub const TAG_U: u8 = 89;
pub const TAG_CITE: u8 = 90;
pub const TAG_DFN: u8 = 91;
pub const TAG_VAR: u8 = 92;
pub const TAG_TIME: u8 = 93;
pub const TAG_BDO: u8 = 94;
pub const TAG_RUBY: u8 = 95;
pub const TAG_RT: u8 = 96;
pub const TAG_RP: u8 = 97;
pub const TAG_DD: u8 = 98;
pub const TAG_DT: u8 = 99;
pub const TAG_ADDRESS: u8 = 100;
pub const TAG_DL: u8 = 101;
pub const TAG_FIGURE: u8 = 102;
pub const TAG_OBJECT: u8 = 103;
pub const TAG_MAIN: u8 = 104;
pub const TAG_HEADER: u8 = 105;
pub const TAG_FIGCAPTION: u8 = 106;
pub const TAG_CAPTION: u8 = 107;
pub const TAG_DATALIST: u8 = 108;
pub const TAG_OPTGROUP: u8 = 109;
pub const TAG_S: u8 = 110;
pub const TAG_STRIKE: u8 = 111;
pub const TAG_NOEMBED: u8 = 112;

pub const MAX_TAG_ID: usize = 113;

/// Reverse lookup: tag ID → static tag name string.
/// Avoids allocating a String for known tags.
pub static TAG_NAMES: [&str; MAX_TAG_ID] = {
  let mut names = [""; MAX_TAG_ID];
  names[TAG_HTML as usize] = "html";
  names[TAG_HEAD as usize] = "head";
  names[TAG_DETAILS as usize] = "details";
  names[TAG_SUMMARY as usize] = "summary";
  names[TAG_TITLE as usize] = "title";
  names[TAG_META as usize] = "meta";
  names[TAG_BR as usize] = "br";
  names[TAG_H1 as usize] = "h1";
  names[TAG_H2 as usize] = "h2";
  names[TAG_H3 as usize] = "h3";
  names[TAG_H4 as usize] = "h4";
  names[TAG_H5 as usize] = "h5";
  names[TAG_H6 as usize] = "h6";
  names[TAG_HR as usize] = "hr";
  names[TAG_STRONG as usize] = "strong";
  names[TAG_B as usize] = "b";
  names[TAG_EM as usize] = "em";
  names[TAG_I as usize] = "i";
  names[TAG_DEL as usize] = "del";
  names[TAG_S as usize] = "s";
  names[TAG_STRIKE as usize] = "strike";
  names[TAG_SUB as usize] = "sub";
  names[TAG_SUP as usize] = "sup";
  names[TAG_INS as usize] = "ins";
  names[TAG_BLOCKQUOTE as usize] = "blockquote";
  names[TAG_CODE as usize] = "code";
  names[TAG_UL as usize] = "ul";
  names[TAG_LI as usize] = "li";
  names[TAG_A as usize] = "a";
  names[TAG_IMG as usize] = "img";
  names[TAG_TABLE as usize] = "table";
  names[TAG_THEAD as usize] = "thead";
  names[TAG_TR as usize] = "tr";
  names[TAG_TH as usize] = "th";
  names[TAG_TD as usize] = "td";
  names[TAG_OL as usize] = "ol";
  names[TAG_PRE as usize] = "pre";
  names[TAG_P as usize] = "p";
  names[TAG_DIV as usize] = "div";
  names[TAG_SPAN as usize] = "span";
  names[TAG_TBODY as usize] = "tbody";
  names[TAG_TFOOT as usize] = "tfoot";
  names[TAG_FORM as usize] = "form";
  names[TAG_NAV as usize] = "nav";
  names[TAG_LABEL as usize] = "label";
  names[TAG_BUTTON as usize] = "button";
  names[TAG_BODY as usize] = "body";
  names[TAG_CENTER as usize] = "center";
  names[TAG_KBD as usize] = "kbd";
  names[TAG_FOOTER as usize] = "footer";
  names[TAG_PATH as usize] = "path";
  names[TAG_SVG as usize] = "svg";
  names[TAG_ARTICLE as usize] = "article";
  names[TAG_SECTION as usize] = "section";
  names[TAG_SCRIPT as usize] = "script";
  names[TAG_STYLE as usize] = "style";
  names[TAG_LINK as usize] = "link";
  names[TAG_AREA as usize] = "area";
  names[TAG_BASE as usize] = "base";
  names[TAG_COL as usize] = "col";
  names[TAG_EMBED as usize] = "embed";
  names[TAG_INPUT as usize] = "input";
  names[TAG_KEYGEN as usize] = "keygen";
  names[TAG_PARAM as usize] = "param";
  names[TAG_SOURCE as usize] = "source";
  names[TAG_TRACK as usize] = "track";
  names[TAG_WBR as usize] = "wbr";
  names[TAG_SELECT as usize] = "select";
  names[TAG_TEXTAREA as usize] = "textarea";
  names[TAG_OPTION as usize] = "option";
  names[TAG_FIELDSET as usize] = "fieldset";
  names[TAG_LEGEND as usize] = "legend";
  names[TAG_AUDIO as usize] = "audio";
  names[TAG_VIDEO as usize] = "video";
  names[TAG_CANVAS as usize] = "canvas";
  names[TAG_IFRAME as usize] = "iframe";
  names[TAG_MAP as usize] = "map";
  names[TAG_DIALOG as usize] = "dialog";
  names[TAG_METER as usize] = "meter";
  names[TAG_PROGRESS as usize] = "progress";
  names[TAG_TEMPLATE as usize] = "template";
  names[TAG_ABBR as usize] = "abbr";
  names[TAG_MARK as usize] = "mark";
  names[TAG_Q as usize] = "q";
  names[TAG_SAMP as usize] = "samp";
  names[TAG_SMALL as usize] = "small";
  names[TAG_NOSCRIPT as usize] = "noscript";
  names[TAG_NOFRAMES as usize] = "noframes";
  names[TAG_XMP as usize] = "xmp";
  names[TAG_PLAINTEXT as usize] = "plaintext";
  names[TAG_ASIDE as usize] = "aside";
  names[TAG_U as usize] = "u";
  names[TAG_CITE as usize] = "cite";
  names[TAG_DFN as usize] = "dfn";
  names[TAG_VAR as usize] = "var";
  names[TAG_TIME as usize] = "time";
  names[TAG_BDO as usize] = "bdo";
  names[TAG_RUBY as usize] = "ruby";
  names[TAG_RT as usize] = "rt";
  names[TAG_RP as usize] = "rp";
  names[TAG_DD as usize] = "dd";
  names[TAG_DT as usize] = "dt";
  names[TAG_ADDRESS as usize] = "address";
  names[TAG_DL as usize] = "dl";
  names[TAG_FIGURE as usize] = "figure";
  names[TAG_OBJECT as usize] = "object";
  names[TAG_MAIN as usize] = "main";
  names[TAG_HEADER as usize] = "header";
  names[TAG_FIGCAPTION as usize] = "figcaption";
  names[TAG_CAPTION as usize] = "caption";
  names[TAG_DATALIST as usize] = "datalist";
  names[TAG_OPTGROUP as usize] = "optgroup";
  names[TAG_NOEMBED as usize] = "noembed";
  names
};

pub const ELEMENT_NODE: u8 = 1;
pub const TEXT_NODE: u8 = 2;

/// Attribute bitmask: which attribute names a tag's rendering actually reads.
/// Each stored value costs an owned `String`, the dominant allocation during
/// parsing, yet a built-in tag reads at most three.
pub const ATTR_NONE: u16 = 0;
pub const ATTR_HREF: u16 = 1 << 0;
pub const ATTR_TITLE: u16 = 1 << 1;
pub const ATTR_ARIA_LABEL: u16 = 1 << 2;
pub const ATTR_SRC: u16 = 1 << 3;
pub const ATTR_ALT: u16 = 1 << 4;
pub const ATTR_CLASS: u16 = 1 << 5;
pub const ATTR_ALIGN: u16 = 1 << 6;
pub const ATTR_NAME: u16 = 1 << 7;
pub const ATTR_PROPERTY: u16 = 1 << 8;
pub const ATTR_CONTENT: u16 = 1 << 9;
pub const ATTR_START: u16 = 1 << 10;
pub const ATTR_COLSPAN: u16 = 1 << 11;
pub const ATTR_ALL: u16 = u16::MAX;

/// Case-insensitive, length-first so the common miss costs one compare.
#[inline]
pub(crate) fn attr_bit(name: &[u8]) -> u16 {
  match name.len() {
    3 => {
      if name.eq_ignore_ascii_case(b"alt") {
        ATTR_ALT
      } else if name.eq_ignore_ascii_case(b"src") {
        ATTR_SRC
      } else {
        ATTR_NONE
      }
    }
    4 => {
      if name.eq_ignore_ascii_case(b"href") {
        ATTR_HREF
      } else if name.eq_ignore_ascii_case(b"name") {
        ATTR_NAME
      } else {
        ATTR_NONE
      }
    }
    5 => {
      if name.eq_ignore_ascii_case(b"title") {
        ATTR_TITLE
      } else if name.eq_ignore_ascii_case(b"class") {
        ATTR_CLASS
      } else if name.eq_ignore_ascii_case(b"align") {
        ATTR_ALIGN
      } else if name.eq_ignore_ascii_case(b"start") {
        ATTR_START
      } else {
        ATTR_NONE
      }
    }
    7 => {
      if name.eq_ignore_ascii_case(b"content") {
        ATTR_CONTENT
      } else if name.eq_ignore_ascii_case(b"colspan") {
        ATTR_COLSPAN
      } else {
        ATTR_NONE
      }
    }
    8 => {
      if name.eq_ignore_ascii_case(b"property") {
        ATTR_PROPERTY
      } else {
        ATTR_NONE
      }
    }
    10 => {
      if name.eq_ignore_ascii_case(b"aria-label") {
        ATTR_ARIA_LABEL
      } else {
        ATTR_NONE
      }
    }
    _ => ATTR_NONE,
  }
}

pub const MARKDOWN_STRONG: &str = "**";
pub const MARKDOWN_EMPHASIS: &str = "*";
pub const MARKDOWN_STRIKETHROUGH: &str = "~~";
pub const MARKDOWN_CODE_BLOCK: &str = "```";
pub const MARKDOWN_INLINE_CODE: &str = "`";
pub const MARKDOWN_HORIZONTAL_RULE: &str = "---";
/// Rule sharing a line with a list marker: `- ---` is dashes and spaces only, so
/// the whole line is a thematic break and the item is lost. `*` cannot collide.
pub const MARKDOWN_HORIZONTAL_RULE_ALT: &str = "***";

pub const NO_SPACING: [u8; 2] = [0, 0];
pub const DEFAULT_BLOCK_SPACING: [u8; 2] = [2, 2];
pub const BLOCKQUOTE_SPACING: [u8; 2] = [2, 2];
pub const LIST_ITEM_SPACING: [u8; 2] = [1, 0];
pub const TABLE_ROW_SPACING: [u8; 2] = [0, 1];

pub const LT_CHAR: u8 = 60; // '<'
pub const GT_CHAR: u8 = 62; // '>'
pub const SLASH_CHAR: u8 = 47; // '/'
pub const EQUALS_CHAR: u8 = 61; // '='
pub const QUOTE_CHAR: u8 = 34; // '"'
pub const APOS_CHAR: u8 = 39; // '\''
pub const EXCLAMATION_CHAR: u8 = 33; // '!'
pub const QUESTION_CHAR: u8 = 63; // '?'
pub const AMPERSAND_CHAR: u8 = 38; // '&'
pub const BACKSLASH_CHAR: u8 = 92; // '\'
pub const DASH_CHAR: u8 = 45; // '-'
pub const SPACE_CHAR: u8 = 32; // ' '
pub const TAB_CHAR: u8 = 9; // '\t'
pub const NEWLINE_CHAR: u8 = 10; // '\n'
pub const CARRIAGE_RETURN_CHAR: u8 = 13; // '\r'
pub const BACKTICK_CHAR: u8 = 96; // '`'

/// Longest built-in HTML tag name (`blockquote`, `figcaption`); also the size
/// of the stack buffer used for the case-insensitive lookup.
const MAX_BUILTIN_TAG_LEN: usize = 10;

#[inline]
pub fn get_tag_id(name: &str) -> Option<u8> {
  get_tag_id_bytes(name.as_bytes())
}

/// Case-insensitive built-in tag lookup over raw bytes.
///
/// Lowercases ASCII into a stack-allocated `MAX_BUILTIN_TAG_LEN` buffer, avoiding the heap
/// allocation `name.to_ascii_lowercase()` would incur when the input contains
/// uppercase bytes. Returns `None` for any tag not in the built-in set.
#[inline]
pub fn get_tag_id_ci_bytes(name: &[u8]) -> Option<u8> {
  let len = name.len();
  if len == 0 || len > MAX_BUILTIN_TAG_LEN {
    return None;
  }
  // Nearly all real-world HTML already uses lowercase tag names. Avoid copying
  // and lowercasing the common case; retain the stack-buffer fallback for valid
  // mixed/uppercase HTML.
  if let Some(id) = get_tag_id_bytes(name) {
    return Some(id);
  }
  let mut buf = [0u8; MAX_BUILTIN_TAG_LEN];
  let mut i = 0;
  while i < len {
    buf[i] = name[i].to_ascii_lowercase();
    i += 1;
  }
  get_tag_id_bytes(&buf[..len])
}

/// Strict, case-sensitive tag name to ID lookup.
///
/// Matches the lowercase tag-name bytes directly against built-in byte-string
/// literals; the compiler lowers this to a length + first-byte decision tree
/// with fixed-size `memcmp` leaves. Returns `None` for any tag not in the
/// built-in set; callers can opt those tags into a rendering via `tagOverrides`.
#[inline]
fn get_tag_id_bytes(bytes: &[u8]) -> Option<u8> {
  Some(match bytes {
    b"figcaption" => TAG_FIGCAPTION,
    b"blockquote" => TAG_BLOCKQUOTE,
    b"plaintext" => TAG_PLAINTEXT,
    b"textarea" => TAG_TEXTAREA,
    b"template" => TAG_TEMPLATE,
    b"progress" => TAG_PROGRESS,
    b"noscript" => TAG_NOSCRIPT,
    b"noframes" => TAG_NOFRAMES,
    b"fieldset" => TAG_FIELDSET,
    b"summary" => TAG_SUMMARY,
    b"section" => TAG_SECTION,
    b"details" => TAG_DETAILS,
    b"caption" => TAG_CAPTION,
    b"datalist" => TAG_DATALIST,
    b"optgroup" => TAG_OPTGROUP,
    b"article" => TAG_ARTICLE,
    b"address" => TAG_ADDRESS,
    b"noembed" => TAG_NOEMBED,
    b"strong" => TAG_STRONG,
    b"source" => TAG_SOURCE,
    b"select" => TAG_SELECT,
    b"script" => TAG_SCRIPT,
    b"option" => TAG_OPTION,
    b"object" => TAG_OBJECT,
    b"legend" => TAG_LEGEND,
    b"keygen" => TAG_KEYGEN,
    b"iframe" => TAG_IFRAME,
    b"header" => TAG_HEADER,
    b"footer" => TAG_FOOTER,
    b"figure" => TAG_FIGURE,
    b"dialog" => TAG_DIALOG,
    b"center" => TAG_CENTER,
    b"canvas" => TAG_CANVAS,
    b"button" => TAG_BUTTON,
    b"strike" => TAG_STRIKE,
    b"video" => TAG_VIDEO,
    b"track" => TAG_TRACK,
    b"title" => TAG_TITLE,
    b"thead" => TAG_THEAD,
    b"tfoot" => TAG_TFOOT,
    b"tbody" => TAG_TBODY,
    b"table" => TAG_TABLE,
    b"style" => TAG_STYLE,
    b"small" => TAG_SMALL,
    b"param" => TAG_PARAM,
    b"meter" => TAG_METER,
    b"label" => TAG_LABEL,
    b"input" => TAG_INPUT,
    b"embed" => TAG_EMBED,
    b"audio" => TAG_AUDIO,
    b"aside" => TAG_ASIDE,
    b"time" => TAG_TIME,
    b"span" => TAG_SPAN,
    b"samp" => TAG_SAMP,
    b"ruby" => TAG_RUBY,
    b"path" => TAG_PATH,
    b"meta" => TAG_META,
    b"mark" => TAG_MARK,
    b"main" => TAG_MAIN,
    b"link" => TAG_LINK,
    b"html" => TAG_HTML,
    b"head" => TAG_HEAD,
    b"form" => TAG_FORM,
    b"code" => TAG_CODE,
    b"cite" => TAG_CITE,
    b"body" => TAG_BODY,
    b"base" => TAG_BASE,
    b"area" => TAG_AREA,
    b"abbr" => TAG_ABBR,
    b"xmp" => TAG_XMP,
    b"wbr" => TAG_WBR,
    b"var" => TAG_VAR,
    b"svg" => TAG_SVG,
    b"sup" => TAG_SUP,
    b"sub" => TAG_SUB,
    b"pre" => TAG_PRE,
    b"nav" => TAG_NAV,
    b"map" => TAG_MAP,
    b"kbd" => TAG_KBD,
    b"ins" => TAG_INS,
    b"img" => TAG_IMG,
    b"div" => TAG_DIV,
    b"dfn" => TAG_DFN,
    b"del" => TAG_DEL,
    b"col" => TAG_COL,
    b"bdo" => TAG_BDO,
    b"ul" => TAG_UL,
    b"tr" => TAG_TR,
    b"th" => TAG_TH,
    b"td" => TAG_TD,
    b"rt" => TAG_RT,
    b"rp" => TAG_RP,
    b"ol" => TAG_OL,
    b"li" => TAG_LI,
    b"hr" => TAG_HR,
    b"h6" => TAG_H6,
    b"h5" => TAG_H5,
    b"h4" => TAG_H4,
    b"h3" => TAG_H3,
    b"h2" => TAG_H2,
    b"h1" => TAG_H1,
    b"em" => TAG_EM,
    b"dt" => TAG_DT,
    b"dl" => TAG_DL,
    b"dd" => TAG_DD,
    b"br" => TAG_BR,
    b"u" => TAG_U,
    b"s" => TAG_S,
    b"q" => TAG_Q,
    b"p" => TAG_P,
    b"i" => TAG_I,
    b"b" => TAG_B,
    b"a" => TAG_A,
    _ => return None,
  })
}