slidev-theme-tud-db 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,9 +53,11 @@ cp -r node_modules/slidev-theme-db/skills/slidev-theme-db .claude/skills/
53
53
  - `components/DbFrame.vue` — shared chrome (logo, footer, padding).
54
54
  - `layouts/` — `cover`, `default`, `two-cols-header`, `two-cols-grid` (wrap `DbFrame`).
55
55
  - `styles/db-theme.css` — tokens, typography, lists, tables (keyed on `.db-default`).
56
- - `setup/shiki.ts` (fenced) and the `THEME` in `vite.config.ts` (inline `{lang}`) —
57
- the **two** highlighters; keep them in sync (both `catppuccin-latte`).
58
- `setup/shiki.ts` also holds the transformer behind `⟦name:text⟧` code marks
59
- (spans a slide's timeline can recolour).
56
+ - `setup/shiki.ts` — code highlighting: the SQL grammar (Shiki's, plus a few
57
+ rules) and the colour themes (catppuccin, upright, with SQL tweaks), built by
58
+ `highlighting()`. Fenced code uses them through Slidev; `vite.config.ts` uses
59
+ the same `highlighting()` for inline `{lang}` code, so the two never drift.
60
+ It also holds the transformer behind `⟦name:text⟧` code marks (spans a
61
+ slide's timeline can recolour).
60
62
  - Link the theme with `link:` (not `file:`), and **restart** the dev server after
61
63
  editing theme files (Vite doesn't hot-reload `node_modules`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidev-theme-tud-db",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "description": "Slidev theme cloning the TU Dresden / Dresden Database Research Group PowerPoint template",
6
6
  "license": "GPL-3.0-or-later",
package/setup/shiki.ts CHANGED
@@ -1,10 +1,12 @@
1
- // Default Shiki color scheme for fenced code blocks (```lang ... ```).
2
- // Matches the inline-code highlighter (see vite.config.ts) and the chair's
3
- // template, so inline and fenced code look the same.
1
+ // Shiki setup for fenced code blocks (```lang ... ```) and magic-move blocks.
2
+ // vite.config.ts highlights inline code with the same grammar and colours (see
3
+ // `highlighting()` below), so inline and fenced code look the same.
4
4
  //
5
5
  // NB: no `import { defineShikiSetup } from '@slidev/types'` on purpose — it's
6
6
  // only a type helper, and Slidev calls this default export as `setup(ctx)`.
7
- // catppuccin-latte / -mocha are bundled with Shiki, so no extra deps needed.
7
+ // The grammar and the colour themes are Shiki's own bundled ones, adjusted
8
+ // below, so no extra deps are needed.
9
+ import { bundledLanguages, bundledThemes } from 'shiki'
8
10
 
9
11
  // Code marks: wrap exact character ranges of a fenced block so a slide can
10
12
  // recolour that text, e.g. at a click. Inside the code, write ⟦name:text⟧: the
@@ -46,138 +48,82 @@ export const codeMarks = {
46
48
  },
47
49
  }
48
50
 
49
- const SQL_FUNCS = /^(COUNT|AVG|SUM|MIN|MAX|COALESCE|ROUND|ABS|CEIL|FLOOR|LENGTH|SUBSTRING|TRIM|UPPER|LOWER|CONCAT|NOW|DATE)$/i
50
- const SQL_CONSTANTS = /^(TRUE|FALSE|UNKNOWN|NULL)$/i
51
- const SQL_KEYWORDS = /^(EXPLAIN|ANALYZE)$/i
52
- const SQL_ALL = /^(TRUE|FALSE|UNKNOWN|NULL|EXPLAIN|ANALYZE|COUNT|AVG|SUM|MIN|MAX|COALESCE|ROUND|ABS|CEIL|FLOOR|LENGTH|SUBSTRING|TRIM|UPPER|LOWER|CONCAT|NOW|DATE)$/i
53
- const SQL_SPLIT = /\b(TRUE|FALSE|UNKNOWN|NULL|EXPLAIN|ANALYZE|COUNT|AVG|SUM|MIN|MAX|COALESCE|ROUND|ABS|CEIL|FLOOR|LENGTH|SUBSTRING|TRIM|UPPER|LOWER|CONCAT|NOW|DATE)\b/i
54
-
55
- function getSqlStyle(w: string) {
56
- if (SQL_FUNCS.test(w)) return { color: '#1E66F5', light: '#1E66F5', dark: '#89B4FA' }
57
- if (SQL_CONSTANTS.test(w)) return { color: '#FE640B', light: '#FE640B', dark: '#FAB387' }
58
- return { color: '#8839EF', light: '#8839EF', dark: '#CBA6F7' }
59
- }
60
-
61
- export const sqlKeywords = {
62
- name: 'tud-db:sql-keywords',
63
- tokens(lines: any[]) {
64
- if ((this as any).options?.lang !== 'sql') return
65
- for (let l = 0; l < lines.length; l++) {
66
- const line = lines[l]
67
- const newTokens: any[] = []
68
- for (const token of line) {
69
- const trimmed = token.content.trim()
70
- const upper = trimmed.toUpperCase()
71
-
72
- // 0. Intercept numeric tokens -> light emerald green (#059669)
73
- if (/^\d+(\.\d+)?$/.test(trimmed)) {
74
- const leading = token.content.slice(0, token.content.indexOf(trimmed))
75
- const trailing = token.content.slice(token.content.indexOf(trimmed) + trimmed.length)
76
- if (leading) newTokens.push({ ...token, content: leading, fontStyle: 0, htmlStyle: { color: '#4C4F69', '--shiki-light': '#4C4F69', '--shiki-dark': '#CDD6F4' } })
77
- newTokens.push({
78
- ...token,
79
- content: trimmed,
80
- color: '#059669',
81
- fontStyle: 0,
82
- htmlStyle: { color: '#059669', '--shiki-light': '#059669', '--shiki-dark': '#34D399' },
83
- })
84
- if (trailing) newTokens.push({ ...token, content: trailing, fontStyle: 0, htmlStyle: { color: '#4C4F69', '--shiki-light': '#4C4F69', '--shiki-dark': '#CDD6F4' } })
85
- continue
86
- }
87
-
88
- // 1. Intercept NULL if Shiki tagged it as keyword purple (including combined tokens like "IS NOT NULL")
89
- if (/\bNULL\b/i.test(token.content)) {
90
- const s = getSqlStyle('NULL')
91
- const parts = token.content.split(/\b(NULL)\b/i)
92
- let currOffset = token.offset
93
- for (const part of parts) {
94
- if (!part) continue
95
- if (/^NULL$/i.test(part)) {
96
- newTokens.push({
97
- ...token,
98
- content: part,
99
- offset: currOffset,
100
- color: s.color,
101
- fontStyle: 0,
102
- htmlStyle: { color: s.color, '--shiki-light': s.light, '--shiki-dark': s.dark },
103
- })
104
- }
105
- else {
106
- newTokens.push({ ...token, content: part, offset: currOffset })
107
- }
108
- currOffset += part.length
109
- }
110
- continue
111
- }
51
+ // SQL: Shiki's grammar, with a few rules added. Being grammar rules, they
52
+ // apply only where SQL code is parsed, never inside comments or strings.
53
+ // - NULL, TRUE, FALSE and UNKNOWN are constants (the grammar knows only NULL,
54
+ // as a keyword, and reads `IS NOT NULL` as a single keyword: here the
55
+ // keyword part and the constant are told apart).
56
+ // - EXPLAIN and ANALYZE are keywords.
57
+ // - A name right before `(` is a function call: `ceil(x)`, `jsonb_agg(…)`;
58
+ // so are common function names on their own, as in an inline `AVG` (the
59
+ // grammar knows functions only before `(`).
60
+ // These rules come last, so the grammar's own reading of a word, such as
61
+ // the keyword in `IN (` or the type in `DECIMAL(10, 2)`, takes precedence.
62
+ const CONSTANTS = 'NULL|TRUE|FALSE|UNKNOWN'
63
+ const FUNCTIONS = 'COUNT|AVG|SUM|MIN|MAX|COALESCE|ROUND|ABS|CEIL|FLOOR|SUBSTRING|TRIM|UPPER|LOWER|CONCAT'
64
+ const SQL_RULES_FIRST = [
65
+ {
66
+ match: `(?i:\\b(IS(?:\\s+NOT)?|NOT)\\s+(${CONSTANTS})\\b)`,
67
+ captures: { 1: { name: 'keyword.other.sql' }, 2: { name: 'constant.language.sql' } },
68
+ },
69
+ { match: `(?i:\\b(?:${CONSTANTS})\\b)`, name: 'constant.language.sql' },
70
+ { match: '(?i:\\b(?:EXPLAIN|ANALYZE)\\b)', name: 'keyword.other.sql' },
71
+ ]
72
+ const SQL_RULES_LAST = [
73
+ { match: '\\b[A-Za-z_][A-Za-z0-9_]*(?=\\()', name: 'support.function.sql' },
74
+ { match: `(?i:\\b(?:${FUNCTIONS})\\b)`, name: 'support.function.sql' },
75
+ ]
112
76
 
113
- // 2. Intercept function tokens (AVG, COUNT, COALESCE, etc.) -> upright blue
114
- // and normalize misclassified identifiers (e.g. `movies` in `CREATE TABLE movies`) -> plain text upright
115
- const hasItalic = token.fontStyle === 1
116
- || token.htmlStyle?.['--shiki-light-font-style'] === 'italic'
117
- || token.htmlStyle?.['--shiki-dark-font-style'] === 'italic'
118
- const isBlue = (token.color?.toLowerCase() === '#1e66f5' || token.htmlStyle?.['--shiki-light']?.toLowerCase() === '#1e66f5')
119
- if (hasItalic && isBlue) {
120
- const s = SQL_ALL.test(trimmed)
121
- ? getSqlStyle(trimmed)
122
- : { color: '#4C4F69', light: '#4C4F69', dark: '#CDD6F4' }
123
- const leading = token.content.slice(0, token.content.indexOf(trimmed))
124
- const trailing = token.content.slice(token.content.indexOf(trimmed) + trimmed.length)
125
- if (leading) newTokens.push({ ...token, content: leading, fontStyle: 0, htmlStyle: { color: '#4C4F69', '--shiki-light': '#4C4F69', '--shiki-dark': '#CDD6F4' } })
126
- newTokens.push({
127
- ...token,
128
- content: trimmed,
129
- color: s.color,
130
- fontStyle: 0,
131
- htmlStyle: { color: s.color, '--shiki-light': s.light, '--shiki-dark': s.dark },
132
- })
133
- if (trailing) newTokens.push({ ...token, content: trailing, fontStyle: 0, htmlStyle: { color: '#4C4F69', '--shiki-light': '#4C4F69', '--shiki-dark': '#CDD6F4' } })
134
- continue
135
- }
77
+ // Colours: catppuccin-latte (light) and -mocha (dark), upright throughout (the
78
+ // template's code has no italics; Shiki's catppuccin sets some, e.g. on
79
+ // function names), with two SQL changes: numbers green, so that they differ
80
+ // from the orange constants (catppuccin colours both orange); and the table
81
+ // name after CREATE TABLE plain text, not a blue function name.
82
+ interface ThemeRule { scope?: string | string[], settings: { foreground?: string, fontStyle?: string } }
83
+ interface Theme { name: string, tokenColors?: ThemeRule[], [key: string]: unknown }
136
84
 
137
- // 3. Intercept plain text tokens containing SQL words (TRUE, FALSE, UNKNOWN, EXPLAIN, ANALYZE, functions, etc.)
138
- const lightColor = token.htmlStyle?.['--shiki-light'] || token.htmlStyle?.color || token.color
139
- const isPlain = !lightColor || lightColor.toLowerCase() === '#4c4f69'
140
- if (isPlain && SQL_SPLIT.test(token.content)) {
141
- const parts = token.content.split(SQL_SPLIT)
142
- let currOffset = token.offset
143
- for (const part of parts) {
144
- if (!part) continue
145
- if (SQL_ALL.test(part)) {
146
- const s = getSqlStyle(part)
147
- newTokens.push({
148
- ...token,
149
- content: part,
150
- offset: currOffset,
151
- color: s.color,
152
- fontStyle: 0,
153
- htmlStyle: { color: s.color, '--shiki-light': s.light, '--shiki-dark': s.dark },
154
- })
155
- }
156
- else {
157
- newTokens.push({
158
- ...token,
159
- content: part,
160
- offset: currOffset,
161
- fontStyle: 0,
162
- htmlStyle: { color: '#4C4F69', '--shiki-light': '#4C4F69', '--shiki-dark': '#CDD6F4' },
163
- })
164
- }
165
- currOffset += part.length
166
- }
167
- continue
168
- }
85
+ function variant(base: Theme, name: string, c: { constant: string, number: string, text: string }): Theme {
86
+ const upright = (base.tokenColors ?? []).map((rule) => {
87
+ const style = rule.settings.fontStyle
88
+ if (!style?.includes('italic'))
89
+ return rule
90
+ return { ...rule, settings: { ...rule.settings, fontStyle: style.replace('italic', '').trim() } }
91
+ })
92
+ return {
93
+ ...base,
94
+ name,
95
+ tokenColors: [
96
+ ...upright,
97
+ { scope: 'constant.language.sql', settings: { foreground: c.constant } },
98
+ { scope: 'constant.numeric.sql', settings: { foreground: c.number } },
99
+ { scope: 'entity.name.function.sql', settings: { foreground: c.text } },
100
+ ],
101
+ }
102
+ }
169
103
 
170
- newTokens.push(token)
171
- }
172
- lines[l] = newTokens
104
+ /** The theme's SQL grammar and its light and dark code colours (loaded once). */
105
+ export const highlighting = (() => {
106
+ let loaded: Promise<{ sql: any, light: Theme, dark: Theme }> | undefined
107
+ return () => loaded ??= (async () => {
108
+ const [[sql], latte, mocha] = await Promise.all([
109
+ bundledLanguages.sql().then(m => m.default as any[]),
110
+ bundledThemes['catppuccin-latte']().then(m => m.default as unknown as Theme),
111
+ bundledThemes['catppuccin-mocha']().then(m => m.default as unknown as Theme),
112
+ ])
113
+ return {
114
+ sql: { ...sql, patterns: [...SQL_RULES_FIRST, ...sql.patterns, ...SQL_RULES_LAST] },
115
+ light: variant(latte, 'tud-db-light', { constant: '#fe640b', number: '#059669', text: '#4c4f69' }),
116
+ dark: variant(mocha, 'tud-db-dark', { constant: '#fab387', number: '#34d399', text: '#cdd6f4' }),
173
117
  }
174
- },
175
- }
118
+ })()
119
+ })()
176
120
 
177
- export default () => ({
178
- themes: {
179
- light: 'catppuccin-latte',
180
- dark: 'catppuccin-mocha',
181
- },
182
- transformers: [codeMarks, sqlKeywords],
183
- })
121
+ export default async () => {
122
+ const { sql, light, dark } = await highlighting()
123
+ return {
124
+ themes: { light, dark },
125
+ // replaces Shiki's `sql` with the extended grammar above
126
+ langs: [sql],
127
+ transformers: [codeMarks],
128
+ }
129
+ }
@@ -215,6 +215,14 @@ code.db-inline-code.with-bg {
215
215
  width: fit-content;
216
216
  max-width: 100%;
217
217
  }
218
+ /* Code that morphs to the next slide (data-morph, from the gsap addon): while
219
+ the morph animates its box from the other slide's size, the code can be
220
+ wider than the box for a moment, and Slidev's `overflow: auto` on code
221
+ blocks would flash a scrollbar. Let it overflow instead; it fits again once
222
+ the morph ends. */
223
+ .slidev-layout [data-morph] .slidev-code {
224
+ overflow: visible;
225
+ }
218
226
  /* Default to transparent background for multi-line blocks, overriding Shiki */
219
227
  .slidev-layout.db-default pre.slidev-code {
220
228
  background: transparent !important;
package/vite.config.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHighlighter } from 'shiki'
2
2
  import { setupTableCaptions } from './markdown.ts'
3
- import { sqlKeywords } from './setup/shiki.ts'
3
+ import { highlighting } from './setup/shiki.ts'
4
4
 
5
5
  // Shipped WITH the theme: Slidev merges vite.config from every root (theme +
6
6
  // addons + deck), so installing this theme is enough — no need to copy a file.
@@ -8,26 +8,40 @@ import { sqlKeywords } from './setup/shiki.ts'
8
8
  // Inline code syntax highlighting. Write inline code as: `{lang} the code`
9
9
  // e.g. `{json} {"a": 1}`
10
10
  // The leading `{lang}` token selects the language; everything after the first
11
- // space is highlighted with Shiki and rendered inside a normal inline <code>.
11
+ // space is highlighted with Shiki and rendered inside a normal inline <code>,
12
+ // with the same SQL grammar and colours as fenced code (setup/shiki.ts).
12
13
  //
13
- // Common SQL keywords/constants/functions (e.g. `NULL`, `TRUE`, `COUNT`) are
14
- // automatically detected and highlighted with the theme's SQL grammar.
14
+ // Untagged inline code is highlighted too when it is clearly one of:
15
+ // - SQL: it has an UPPERCASE SQL word (`NULL`, `GROUP BY`, `TRUE AND UNKNOWN`)
16
+ // or is a lone comparison operator (`<>`, `<=>`). Uppercase only, so that
17
+ // code from other languages (`std::set`, `end()`, `{lines:true}`) stays plain.
18
+ // - a function call, `next()` or `child.next()`: the name in function blue.
15
19
  //
16
- // A deck can still override THEME, LANGS, or the rule by defining its own
20
+ // A deck can still override LANGS or the rule by defining its own
17
21
  // vite.config.ts (the deck's config merges last and wins).
18
- const THEME = 'catppuccin-latte'
19
- const LANGS = ['json', 'yaml', 'sql', 'c', 'cpp', 'python']
22
+ const LANGS = ['json', 'yaml', 'c', 'cpp', 'python']
20
23
 
21
- const AUTO_SQL = /\b(TRUE|FALSE|UNKNOWN|NULL|EXPLAIN|ANALYZE|COUNT|AVG|SUM|MIN|MAX|COALESCE|SELECT|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|JOIN|LEFT|RIGHT|INNER|OUTER|FULL|CROSS|ON|USING|LIMIT|OFFSET|UNION|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|TABLE|INTO|VALUES|SET|BETWEEN|IN|NOT|AND|OR|IS|LIKE|ILIKE|AS|CASE|WHEN|THEN|ELSE|END)\b/i
24
+ const AUTO_SQL = /\b(?:TRUE|FALSE|UNKNOWN|NULL|EXPLAIN|ANALYZE|COUNT|AVG|SUM|MIN|MAX|COALESCE|SELECT|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|JOIN|LEFT|RIGHT|INNER|OUTER|FULL|CROSS|ON|USING|LIMIT|OFFSET|UNION|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|TABLE|INTO|VALUES|SET|BETWEEN|IN|NOT|AND|OR|IS|LIKE|ILIKE|AS|CASE|WHEN|THEN|ELSE|END)\b/
22
25
  const AUTO_SQL_OPS = /^(?:<=>|<>|<=|>=|!=|=|<|>)$/
26
+ const CALL = /^((?:[a-z_]\w*\.)*)([a-z_]\w*)(\s*\(.*\))$/i
23
27
 
24
- function isAutoSql(code: string): boolean {
25
- const trimmed = code.trim()
26
- return AUTO_SQL_OPS.test(trimmed) || AUTO_SQL.test(trimmed)
28
+ const escapeHtml = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
29
+
30
+ // Vue compiles each slide as a template and condenses whitespace: it drops a
31
+ // text node that is only spaces at the start or end of an element (Shiki's
32
+ // span for a lone space), and shrinks runs of spaces to one. Protect exactly
33
+ // those spaces with &nbsp;; every other space stays a normal one, so long
34
+ // inline code can still wrap.
35
+ function keepSpaces(html: string): string {
36
+ return html
37
+ .replace(/>( +)</g, (_m, sp: string) => `>${'&nbsp;'.repeat(sp.length)}<`)
38
+ .replace(/ {2,}/g, sp => `${'&nbsp;'.repeat(sp.length - 1)} `)
27
39
  }
28
40
 
29
41
  export default async () => {
30
- const highlighter = await createHighlighter({ themes: [THEME], langs: LANGS })
42
+ const { sql, light } = await highlighting()
43
+ const highlighter = await createHighlighter({ themes: [light], langs: [...LANGS, sql] })
44
+ const THEME = light.name
31
45
 
32
46
  return {
33
47
  slidev: {
@@ -39,45 +53,34 @@ export default async () => {
39
53
  md.renderer.rules.code_inline = (tokens: any[], idx: number, options: any, env: any, self: any) => {
40
54
  const raw = String(tokens[idx].content)
41
55
  const m = raw.match(/^\{([\w-]+)\}\s+([\s\S]+)$/)
42
- let lang = m ? m[1] : null
43
- let code = m ? m[2] : raw
44
-
45
- // Auto-detect common SQL keywords/constants when untagged
46
- if (!lang && isAutoSql(code)) {
47
- lang = 'sql'
48
- }
56
+ const code = m ? m[2] : raw
57
+ const bare = code.trim()
58
+ const lang = m ? m[1] : (AUTO_SQL_OPS.test(bare) || AUTO_SQL.test(bare)) ? 'sql' : null
49
59
 
50
- // Auto-detect function calls like `next()` or `child.next()` when untagged -> blue upright
51
- const fnMatch = !lang && code.trim().match(/^((?:[a-zA-Z_]\w*\.)*)([a-zA-Z_]\w*)\s*(\(.*?\))$/)
52
- if (fnMatch) {
53
- const prefix = fnMatch[1]
54
- const fnName = fnMatch[2]
55
- const rest = fnMatch[3].replace(/ /g, '&nbsp;')
56
- let inner = ''
57
- if (prefix) {
58
- inner += `<span style="color:#4C4F69;--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">${prefix}</span>`
59
- }
60
- inner += `<span style="color:#1E66F5;--shiki-light:#1E66F5;--shiki-dark:#89B4FA">${fnName}</span>`
61
- inner += `<span style="color:#7C7F93;--shiki-light:#7C7F93;--shiki-dark:#9399B2">${rest}</span>`
62
- return `<code class="db-inline-code">${inner}</code>`
60
+ // untagged function call: the name in function blue, the rest in
61
+ // the colour of punctuation (as Shiki colours `()` in code)
62
+ const call = !lang && bare.match(CALL)
63
+ if (call) {
64
+ const [, prefix, name, rest] = call
65
+ const span = (color: string, text: string) => text ? `<span style="color:${color}">${escapeHtml(text)}</span>` : ''
66
+ const inner = span('#4C4F69', prefix) + span('#1E66F5', name) + span('#7C7F93', rest)
67
+ return `<code class="db-inline-code">${keepSpaces(inner)}</code>`
63
68
  }
64
69
 
65
70
  if (lang && highlighter.getLoadedLanguages().includes(lang)) {
66
- let inner = highlighter
67
- .codeToHtml(code, { lang, theme: THEME, transformers: [sqlKeywords] })
71
+ const inner = highlighter
72
+ .codeToHtml(code, { lang, theme: THEME })
68
73
  .replace(/^<pre[^>]*><code[^>]*>/, '')
69
74
  .replace(/<\/code><\/pre>\s*$/, '')
70
-
71
- // Preserve all spaces across tokens so Vue's template compiler never condenses/strips them
72
- inner = inner.replace(/<span([^>]*)>([^<]*)<\/span>/g, (_m, attrs, text) => {
73
- return `<span${attrs}>${text.replace(/ /g, '&nbsp;')}</span>`
74
- })
75
-
76
- return `<code class="db-inline-code">${inner}</code>`
75
+ return `<code class="db-inline-code">${keepSpaces(inner)}</code>`
77
76
  }
78
- return fallback
77
+ // plain inline code: unhighlighted, but its spaces kept too. (Slidev
78
+ // renders markdown asynchronously, so the default rule may return
79
+ // a promise of the HTML.)
80
+ const plain = fallback
79
81
  ? fallback(tokens, idx, options, env, self)
80
82
  : self.renderToken(tokens, idx, options)
83
+ return typeof plain === 'string' ? keepSpaces(plain) : Promise.resolve(plain).then(keepSpaces)
81
84
  }
82
85
 
83
86
  // ASCII arrows -> unicode, in plain TEXT only. Code spans/blocks are