@dorsk/tsumikit 0.80.0 → 0.80.2
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/dist/components/atoms/Artwork.svelte +14 -10
- package/dist/components/atoms/Artwork.svelte.d.ts +1 -1
- package/dist/components/atoms/Link.svelte +27 -19
- package/dist/components/layouts/NavBar.svelte +49 -2
- package/dist/components/layouts/NavBar.svelte.d.ts +7 -0
- package/dist/components/molecules/LookPicker.svelte +15 -2
- package/dist/components/molecules/SectionHeader.svelte +2 -0
- package/dist/components/molecules/ThemePicker.svelte +8 -3
- package/dist/looks/tactile.css +2 -2
- package/dist/markdown.js +47 -21
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// initials (or an icon) when there is no source or it fails to load. Width
|
|
5
5
|
// comes from `size`, height from `aspect`; `status` overlays a Dot/Badge in
|
|
6
6
|
// the bottom-right corner.
|
|
7
|
-
import type
|
|
7
|
+
import { untrack, type Snippet } from 'svelte';
|
|
8
8
|
import { artworkGradient, initials as initialsOf } from '../../artwork';
|
|
9
9
|
import Icon from './Icon.svelte';
|
|
10
10
|
import type { IconName } from './Icon.svelte';
|
|
@@ -51,23 +51,27 @@
|
|
|
51
51
|
...rest
|
|
52
52
|
}: Omit<HTMLAttributes<HTMLElement>, keyof Own> & Own = $props();
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
$
|
|
56
|
-
src;
|
|
57
|
-
srcset;
|
|
58
|
-
failed = false;
|
|
59
|
-
});
|
|
54
|
+
const source = $derived(`${src ?? ''} ${srcset ?? ''}`);
|
|
55
|
+
let failedSource = $state<string | null>(null);
|
|
60
56
|
|
|
61
57
|
const key = $derived(seed ?? alt);
|
|
62
|
-
const showImage = $derived(Boolean(src) &&
|
|
58
|
+
const showImage = $derived(Boolean(src) && failedSource !== source);
|
|
63
59
|
const tile = $derived(showImage ? undefined : alt.trim() ? 'named' : 'decorative');
|
|
64
60
|
const gradient = $derived(showImage ? undefined : artworkGradient(key));
|
|
65
61
|
const text = $derived(fallback === 'initials' ? initialsOf(key) : '');
|
|
66
62
|
|
|
67
63
|
function fail() {
|
|
68
|
-
|
|
64
|
+
failedSource = source;
|
|
69
65
|
onerror?.();
|
|
70
66
|
}
|
|
67
|
+
|
|
68
|
+
function watchLoad(img: HTMLImageElement) {
|
|
69
|
+
img.addEventListener('error', fail);
|
|
70
|
+
untrack(() => {
|
|
71
|
+
if (img.complete && img.naturalWidth === 0) fail();
|
|
72
|
+
});
|
|
73
|
+
return () => img.removeEventListener('error', fail);
|
|
74
|
+
}
|
|
71
75
|
</script>
|
|
72
76
|
|
|
73
77
|
<div
|
|
@@ -83,7 +87,7 @@
|
|
|
83
87
|
{...rest}
|
|
84
88
|
>
|
|
85
89
|
{#if showImage}
|
|
86
|
-
<img {src} {srcset} {sizes} {alt} class="img fit-{fit}" loading="lazy" decoding="async"
|
|
90
|
+
<img {src} {srcset} {sizes} {alt} class="img fit-{fit}" loading="lazy" decoding="async" {@attach watchLoad} />
|
|
87
91
|
{:else if fallback === 'initials' && text}
|
|
88
92
|
<span class="initials" aria-hidden="true">{text}</span>
|
|
89
93
|
{:else if fallback === 'icon'}
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
{href}
|
|
36
36
|
class="link tone-{tone} underline-{underline} align-{align} {klass}"
|
|
37
37
|
data-tsu="Link"
|
|
38
|
-
{...rest}>{@render children?.()}</a
|
|
38
|
+
{...rest}><span class="link-text"><span class="link-ink">{@render children?.()}</span></span></a
|
|
39
39
|
>
|
|
40
40
|
{:else}
|
|
41
41
|
<button
|
|
42
42
|
class="link tone-{tone} underline-{underline} align-{align} {klass}"
|
|
43
43
|
data-tsu="Link"
|
|
44
44
|
{...rest}
|
|
45
|
-
type="button">{@render children?.()}</button
|
|
45
|
+
type="button"><span class="link-text"><span class="link-ink">{@render children?.()}</span></span></button
|
|
46
46
|
>
|
|
47
47
|
{/if}
|
|
48
48
|
|
|
@@ -102,44 +102,52 @@
|
|
|
102
102
|
--link-hold: var(--dur-instant);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/* Sweep line: the underline fills in from the left and leaves to the right.
|
|
105
|
+
/* Sweep line: the underline fills in from the left and leaves to the right. Both variants paint
|
|
106
|
+
on the inline .link-ink so they follow the text whatever box the app gives .link. */
|
|
106
107
|
.link:not(.underline-none):not(:global([data-fx-link]:not([data-fx-link='sweep'])) *),
|
|
107
108
|
:global([data-fx-link='sweep']) .link:not(.underline-none):not(:global([data-fx-link='sweep'] [data-fx-link]:not([data-fx-link='sweep'])) *) {
|
|
108
109
|
--link-base: 0%;
|
|
109
110
|
text-decoration: none;
|
|
110
|
-
background-image:
|
|
111
|
-
linear-gradient(currentColor, currentColor),
|
|
112
|
-
linear-gradient(color-mix(in srgb, currentColor 30%, transparent), color-mix(in srgb, currentColor 30%, transparent));
|
|
113
|
-
background-repeat: no-repeat;
|
|
114
|
-
background-position: var(--link-on, 100%) 100%, 0 100%;
|
|
115
|
-
background-size: var(--link-draw, 0%) 1.5px, var(--link-base) 1px;
|
|
116
|
-
transition:
|
|
117
|
-
color var(--dur-fast) ease,
|
|
118
|
-
background-size var(--dur-fast) var(--ease-out);
|
|
119
111
|
&.underline-always {
|
|
120
112
|
--link-base: 100%;
|
|
121
113
|
}
|
|
114
|
+
.link-ink {
|
|
115
|
+
background-image:
|
|
116
|
+
linear-gradient(currentColor, currentColor),
|
|
117
|
+
linear-gradient(color-mix(in srgb, currentColor 30%, transparent), color-mix(in srgb, currentColor 30%, transparent));
|
|
118
|
+
background-repeat: no-repeat;
|
|
119
|
+
background-position: var(--link-on, 100%) 100%, 0 100%;
|
|
120
|
+
background-size: var(--link-draw, 0%) 1.5px, var(--link-base) 1px;
|
|
121
|
+
transition: background-size var(--dur-fast) var(--ease-out);
|
|
122
|
+
}
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
/* Highlighter: a marker swipes in behind the words and fades on leave. */
|
|
125
126
|
:global([data-fx-link='highlight']) .link:not(.underline-none):not(:global([data-fx-link='highlight'] [data-fx-link]:not([data-fx-link='highlight'])) *) {
|
|
126
127
|
margin-inline: -0.18em;
|
|
127
128
|
padding-inline: 0.18em;
|
|
128
|
-
border-radius: 0.2em 0.35em 0.25em 0.4em;
|
|
129
|
-
background-image: linear-gradient(100deg, var(--link-mark), color-mix(in srgb, var(--link-mark) 70%, transparent));
|
|
130
|
-
background-repeat: no-repeat;
|
|
131
|
-
background-size: var(--link-draw, 0%) 100%;
|
|
132
129
|
text-decoration-color: var(--link-deco, color-mix(in srgb, currentColor 45%, transparent));
|
|
133
130
|
text-underline-offset: 0.2em;
|
|
134
131
|
transition:
|
|
135
132
|
color var(--dur-fast) ease,
|
|
136
133
|
text-decoration-color var(--dur-fast) ease,
|
|
137
|
-
--link-mark var(--link-in, var(--dur-exit)) ease
|
|
138
|
-
|
|
134
|
+
--link-mark var(--link-in, var(--dur-exit)) ease;
|
|
135
|
+
.link-text,
|
|
136
|
+
.link-ink {
|
|
137
|
+
margin-inline: -0.18em;
|
|
138
|
+
padding-inline: 0.18em;
|
|
139
|
+
}
|
|
140
|
+
.link-ink {
|
|
141
|
+
border-radius: 0.2em 0.35em 0.25em 0.4em;
|
|
142
|
+
background-image: linear-gradient(100deg, var(--link-mark), color-mix(in srgb, var(--link-mark) 70%, transparent));
|
|
143
|
+
background-repeat: no-repeat;
|
|
144
|
+
background-size: var(--link-draw, 0%) 100%;
|
|
145
|
+
transition: background-size var(--link-grow, var(--dur-instant)) var(--ease-out) var(--link-hold, var(--dur-exit));
|
|
146
|
+
}
|
|
139
147
|
}
|
|
140
148
|
@property --link-mark {
|
|
141
149
|
syntax: '<color>';
|
|
142
|
-
inherits:
|
|
150
|
+
inherits: true;
|
|
143
151
|
initial-value: transparent;
|
|
144
152
|
}
|
|
145
153
|
@media (pointer: coarse) {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
placement = 'inline',
|
|
39
39
|
orientation = 'stacked',
|
|
40
40
|
variant = 'flush',
|
|
41
|
+
labels = 'fit',
|
|
41
42
|
maxWidth,
|
|
42
43
|
class: klass = '',
|
|
43
44
|
style: styleProp = '',
|
|
@@ -57,6 +58,13 @@
|
|
|
57
58
|
orientation?: 'stacked' | 'inline';
|
|
58
59
|
/** 'floating' = rounded elevated card inset from the edges. */
|
|
59
60
|
variant?: 'flush' | 'floating';
|
|
61
|
+
/**
|
|
62
|
+
* Stacked labels that run out of room. 'fit' shrinks the font down to
|
|
63
|
+
* `--navbar-label-min` (10px) so the longest word fits, then wraps to two
|
|
64
|
+
* lines, then ellipsis. 'truncate' = one line with ellipsis at full size.
|
|
65
|
+
* 'full' never clips: labels spill past their item.
|
|
66
|
+
*/
|
|
67
|
+
labels?: 'fit' | 'truncate' | 'full';
|
|
60
68
|
/** Caps and centres the row of items. */
|
|
61
69
|
maxWidth?: string;
|
|
62
70
|
class?: string;
|
|
@@ -64,6 +72,23 @@
|
|
|
64
72
|
[key: string]: unknown;
|
|
65
73
|
} = $props();
|
|
66
74
|
|
|
75
|
+
// Widest word across the labels in em, rounded up from typical UI sans metrics.
|
|
76
|
+
const charEm = (c: string) =>
|
|
77
|
+
/[ijlI.,:;!|']/.test(c) ? 0.28
|
|
78
|
+
: /[frtJ]/.test(c) ? 0.4
|
|
79
|
+
: /[mwMW]/.test(c) ? 0.94
|
|
80
|
+
: /[A-Z]/.test(c) ? 0.72
|
|
81
|
+
: /[0-9]/.test(c) ? 0.58
|
|
82
|
+
: /[a-z]/.test(c) ? 0.6
|
|
83
|
+
: c >= '\u2e80' ? 1
|
|
84
|
+
: 0.62;
|
|
85
|
+
const labelEm = $derived(
|
|
86
|
+
Math.max(
|
|
87
|
+
1,
|
|
88
|
+
...items.flatMap((item) => item.label.split(/\s+/).map((w) => [...w].reduce((sum, c) => sum + charEm(c), 0)))
|
|
89
|
+
).toFixed(2)
|
|
90
|
+
);
|
|
91
|
+
|
|
67
92
|
const hasBadge = (badge: NavBarItem['badge']) =>
|
|
68
93
|
badge !== undefined && badge !== null && badge !== '' && badge !== 0;
|
|
69
94
|
|
|
@@ -151,11 +176,11 @@
|
|
|
151
176
|
<nav
|
|
152
177
|
data-tsu="NavBar"
|
|
153
178
|
aria-label={label}
|
|
154
|
-
class="navbar {klass}"
|
|
179
|
+
class="navbar labels-{labels} {klass}"
|
|
155
180
|
class:bar={placement === 'bar'}
|
|
156
181
|
class:floating={variant === 'floating'}
|
|
157
182
|
class:stacked={orientation === 'stacked'}
|
|
158
|
-
style="{maxWidth ?
|
|
183
|
+
style="--navbar-label-em: {labelEm};{maxWidth ? ` --navbar-max: ${maxWidth};` : ''}{styleProp}"
|
|
159
184
|
{...rest}
|
|
160
185
|
>
|
|
161
186
|
<div class="navbar-row">
|
|
@@ -314,6 +339,28 @@
|
|
|
314
339
|
.stacked .navbar-label {
|
|
315
340
|
max-width: 100%;
|
|
316
341
|
}
|
|
342
|
+
.stacked.labels-fit :is(.navbar-inner, .navbar-side) {
|
|
343
|
+
gap: 0;
|
|
344
|
+
}
|
|
345
|
+
.stacked.labels-fit .navbar-link {
|
|
346
|
+
container-type: inline-size;
|
|
347
|
+
contain-intrinsic-inline-size: 4rem;
|
|
348
|
+
padding-inline: 0;
|
|
349
|
+
}
|
|
350
|
+
.stacked.labels-fit .navbar-label {
|
|
351
|
+
display: -webkit-box;
|
|
352
|
+
-webkit-box-orient: vertical;
|
|
353
|
+
-webkit-line-clamp: 2;
|
|
354
|
+
line-clamp: 2;
|
|
355
|
+
font-size: clamp(var(--navbar-label-min, calc(0.625rem * var(--fs-scale))), 100cqi / var(--navbar-label-em), 1em);
|
|
356
|
+
line-height: var(--lh-tight);
|
|
357
|
+
text-align: center;
|
|
358
|
+
white-space: normal;
|
|
359
|
+
}
|
|
360
|
+
.labels-full .navbar-label {
|
|
361
|
+
max-width: none;
|
|
362
|
+
overflow: visible;
|
|
363
|
+
}
|
|
317
364
|
.navbar-badge {
|
|
318
365
|
position: absolute;
|
|
319
366
|
top: -0.35rem;
|
|
@@ -33,6 +33,13 @@ type $$ComponentProps = {
|
|
|
33
33
|
orientation?: 'stacked' | 'inline';
|
|
34
34
|
/** 'floating' = rounded elevated card inset from the edges. */
|
|
35
35
|
variant?: 'flush' | 'floating';
|
|
36
|
+
/**
|
|
37
|
+
* Stacked labels that run out of room. 'fit' shrinks the font down to
|
|
38
|
+
* `--navbar-label-min` (10px) so the longest word fits, then wraps to two
|
|
39
|
+
* lines, then ellipsis. 'truncate' = one line with ellipsis at full size.
|
|
40
|
+
* 'full' never clips: labels spill past their item.
|
|
41
|
+
*/
|
|
42
|
+
labels?: 'fit' | 'truncate' | 'full';
|
|
36
43
|
/** Caps and centres the row of items. */
|
|
37
44
|
maxWidth?: string;
|
|
38
45
|
class?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { ComponentProps } from 'svelte';
|
|
3
3
|
import SegmentedControl, { type SegmentOption } from './SegmentedControl.svelte';
|
|
4
|
+
import { browser } from '../../env';
|
|
4
5
|
import { theme } from '../../stores/theme.svelte';
|
|
5
6
|
import { LOOKS, type LookId } from '../../theme-mode';
|
|
6
7
|
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
</script>
|
|
41
42
|
|
|
42
43
|
{#snippet option(o: SegmentOption)}
|
|
43
|
-
<span class="look">
|
|
44
|
+
<span class="look" data-look-option={o.value}>
|
|
44
45
|
<span class="name">{o.label}</span>
|
|
45
46
|
{#if experimentalLabel && tagged.has(o.value)}<span class="tag">{experimentalLabel}</span>{/if}
|
|
46
47
|
</span>
|
|
@@ -48,7 +49,7 @@
|
|
|
48
49
|
|
|
49
50
|
<SegmentedControl
|
|
50
51
|
{options}
|
|
51
|
-
value={theme.look}
|
|
52
|
+
value={browser ? theme.look : undefined}
|
|
52
53
|
onchange={(v) => {
|
|
53
54
|
theme.setLook(v);
|
|
54
55
|
onchange?.(theme.look);
|
|
@@ -75,6 +76,18 @@
|
|
|
75
76
|
overflow: hidden;
|
|
76
77
|
text-overflow: ellipsis;
|
|
77
78
|
}
|
|
79
|
+
/* Prerendered markup selects nothing; until hydration the look prepainted on <html> shows as selected. */
|
|
80
|
+
:global(:root:not([data-look]) .look-picker:not(:has(.selected)) .seg-item:has(> [data-look-option='default'])),
|
|
81
|
+
:global(:root[data-look='depth'] .look-picker:not(:has(.selected)) .seg-item:has(> [data-look-option='depth'])),
|
|
82
|
+
:global(:root[data-look='tactile'] .look-picker:not(:has(.selected)) .seg-item:has(> [data-look-option='tactile'])),
|
|
83
|
+
:global(:root[data-look='editorial'] .look-picker:not(:has(.selected)) .seg-item:has(> [data-look-option='editorial'])) {
|
|
84
|
+
color: var(--text);
|
|
85
|
+
background: var(--thumb-fill);
|
|
86
|
+
box-shadow: var(--thumb-shadow);
|
|
87
|
+
}
|
|
88
|
+
:global(:root[data-look='editorial'] .look-picker:not(:has(.selected)) .seg-item:has(> [data-look-option='editorial'])) {
|
|
89
|
+
color: var(--bg);
|
|
90
|
+
}
|
|
78
91
|
.tag {
|
|
79
92
|
flex: none;
|
|
80
93
|
padding: 0 var(--sp-1);
|
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
// (--c-bg, --c-surface, --c-text, --c-accent; the derived --bg etc. are
|
|
6
6
|
// resolved once at :root and would not re-scope). The root default theme
|
|
7
7
|
// has no [data-theme] block, so its swatch carries the :root values.
|
|
8
|
+
// Prerendered, the trigger swatch is unscoped: it shows the theme prepainted
|
|
9
|
+
// on <html> until hydration.
|
|
8
10
|
import type { ComponentProps } from 'svelte';
|
|
9
11
|
import type { AnchorAttributes } from '../../anchor';
|
|
10
12
|
import Popover from './Popover.svelte';
|
|
13
|
+
import { browser } from '../../env';
|
|
11
14
|
import { type ThemeDef, theme } from '../../stores/theme.svelte';
|
|
12
15
|
import { AUTO_THEME } from '../../theme-mode';
|
|
13
16
|
|
|
@@ -63,7 +66,9 @@
|
|
|
63
66
|
);
|
|
64
67
|
const named = (id: string) => theme.all.find((t) => t.id === id)?.label ?? id;
|
|
65
68
|
const autoCaption = $derived(`${autoLabel} · ${named(theme.pref.light)} / ${named(theme.pref.dark)}`);
|
|
66
|
-
const title = $derived(
|
|
69
|
+
const title = $derived(
|
|
70
|
+
!browser ? 'Theme' : isAuto ? `${autoLabel} · ${theme.label}` : `Theme: ${theme.label}`
|
|
71
|
+
);
|
|
67
72
|
const caption = $derived(
|
|
68
73
|
hoveredAuto || (isAuto && !hovered)
|
|
69
74
|
? autoCaption
|
|
@@ -71,7 +76,7 @@
|
|
|
71
76
|
);
|
|
72
77
|
</script>
|
|
73
78
|
|
|
74
|
-
{#snippet swatch(id: string)}
|
|
79
|
+
{#snippet swatch(id: string | undefined)}
|
|
75
80
|
<span class="swatch" data-theme={id} aria-hidden="true">
|
|
76
81
|
<i class="q bg"></i><i class="q surface"></i><i class="q text"></i><i class="q accent"></i>
|
|
77
82
|
</span>
|
|
@@ -94,7 +99,7 @@
|
|
|
94
99
|
triggerClass={klass}
|
|
95
100
|
style={styleProp}
|
|
96
101
|
>
|
|
97
|
-
{#snippet trigger()}<span class="trigger" data-tsu="ThemePicker" {title}>{@render swatch(theme.current)}{#if isAuto}<span class="auto-dot" aria-hidden="true">◐</span>{/if}</span>{/snippet}
|
|
102
|
+
{#snippet trigger()}<span class="trigger" data-tsu="ThemePicker" {title}>{@render swatch(browser ? theme.current : undefined)}{#if isAuto}<span class="auto-dot" aria-hidden="true">◐</span>{/if}</span>{/snippet}
|
|
98
103
|
<div class="panel">
|
|
99
104
|
{#each groups as g (g.mode)}
|
|
100
105
|
<div class="group-label">{g.label}</div>
|
package/dist/looks/tactile.css
CHANGED
|
@@ -316,12 +316,12 @@
|
|
|
316
316
|
transition-duration: var(--dur-press);
|
|
317
317
|
transition-timing-function: var(--ease-out);
|
|
318
318
|
}
|
|
319
|
-
:root[data-look="tactile"] .row {
|
|
319
|
+
:root[data-look="tactile"] [data-tsu="RadioGroup"] .row {
|
|
320
320
|
border-color: transparent;
|
|
321
321
|
border-radius: var(--r-lg);
|
|
322
322
|
background: var(--bg-elevated-2);
|
|
323
323
|
}
|
|
324
|
-
:root[data-look="tactile"] .row.selected {
|
|
324
|
+
:root[data-look="tactile"] [data-tsu="RadioGroup"] .row.selected {
|
|
325
325
|
border-color: transparent;
|
|
326
326
|
background: color-mix(in oklab, var(--accent) 12%, var(--bg-elevated));
|
|
327
327
|
}
|
package/dist/markdown.js
CHANGED
|
@@ -89,7 +89,9 @@ function inline(s, autolink) {
|
|
|
89
89
|
function splitRow(row) {
|
|
90
90
|
const cells = [];
|
|
91
91
|
let cur = '';
|
|
92
|
-
|
|
92
|
+
let t = row.trim();
|
|
93
|
+
if (t.startsWith('|'))
|
|
94
|
+
t = t.slice(1);
|
|
93
95
|
for (let i = 0; i < t.length; i++) {
|
|
94
96
|
if (t[i] === '\\' && t[i + 1] === '|') {
|
|
95
97
|
cur += '|';
|
|
@@ -102,32 +104,57 @@ function splitRow(row) {
|
|
|
102
104
|
else
|
|
103
105
|
cur += t[i];
|
|
104
106
|
}
|
|
105
|
-
|
|
107
|
+
if (cur.trim() || !cells.length)
|
|
108
|
+
cells.push(cur);
|
|
106
109
|
return cells;
|
|
107
110
|
}
|
|
108
|
-
const
|
|
111
|
+
const hasPipe = (line) => /(?:^|[^\\])\|/.test(line);
|
|
112
|
+
const DELIM_CELL = /^\s*:?-+:?\s*$/;
|
|
113
|
+
const TABLE_BREAK = new RegExp(`^\\s*(?:$|#{1,6}(?:\\s|$)|>|(?:[-*+]|\\d{1,9}[.)])\\s|(?:-{3,}|\\*{3,}|_{3,})\\s*$|${BLOCK_L}\\d+${BLOCK_R}\\s*$)`);
|
|
109
114
|
function table(header, delim, body, autolink) {
|
|
110
|
-
const aligns =
|
|
115
|
+
const aligns = delim.map((c) => {
|
|
111
116
|
const l = c.trim().startsWith(':');
|
|
112
117
|
const r = c.trim().endsWith(':');
|
|
113
118
|
return r && l ? 'center' : r ? 'right' : l ? 'left' : '';
|
|
114
119
|
});
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
120
|
+
const row = (line, tag) => {
|
|
121
|
+
const cells = splitRow(line);
|
|
122
|
+
const html = aligns.map((a, i) => {
|
|
123
|
+
const style = a ? ` style="text-align:${a}"` : '';
|
|
124
|
+
return `<${tag}${style}>${inline((cells[i] ?? '').trim(), autolink)}</${tag}>`;
|
|
125
|
+
});
|
|
126
|
+
return `<tr>${html.join('')}</tr>`;
|
|
118
127
|
};
|
|
119
|
-
const head =
|
|
120
|
-
|
|
121
|
-
.join('')}</tr>`;
|
|
122
|
-
const rows = body
|
|
123
|
-
.split('\n')
|
|
124
|
-
.filter((r) => r.trim())
|
|
125
|
-
.map((r) => `<tr>${splitRow(r)
|
|
126
|
-
.map((c, i) => cell(c, i, 'td'))
|
|
127
|
-
.join('')}</tr>`)
|
|
128
|
-
.join('');
|
|
128
|
+
const head = row(header, 'th');
|
|
129
|
+
const rows = body.map((r) => row(r, 'td')).join('');
|
|
129
130
|
return `<table><thead>${head}</thead><tbody>${rows}</tbody></table>`;
|
|
130
131
|
}
|
|
132
|
+
// GFM pipe tables on escaped text: a header row, a delimiter row with the
|
|
133
|
+
// same cell count, then rows up to a blank line or the start of another block.
|
|
134
|
+
function tables(s, saved, autolink) {
|
|
135
|
+
const lines = s.split('\n');
|
|
136
|
+
const out = [];
|
|
137
|
+
for (let i = 0; i < lines.length; i++) {
|
|
138
|
+
const head = lines[i];
|
|
139
|
+
const next = lines[i + 1];
|
|
140
|
+
if (next === undefined || !hasPipe(head) || !hasPipe(next) || TABLE_BREAK.test(head)) {
|
|
141
|
+
out.push(head);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const delim = splitRow(next);
|
|
145
|
+
const cols = splitRow(head);
|
|
146
|
+
if (!delim.every((c) => DELIM_CELL.test(c)) || cols.length !== delim.length) {
|
|
147
|
+
out.push(head);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
let j = i + 2;
|
|
151
|
+
while (j < lines.length && !TABLE_BREAK.test(lines[j]))
|
|
152
|
+
j++;
|
|
153
|
+
out.push('', stash(saved, table(head, delim, lines.slice(i + 2, j), autolink)), '');
|
|
154
|
+
i = j - 1;
|
|
155
|
+
}
|
|
156
|
+
return out.join('\n');
|
|
157
|
+
}
|
|
131
158
|
// Block pass over the escaped, inline-rendered lines. Produces paragraphs,
|
|
132
159
|
// headings, lists, blockquotes and rules; stashed blocks stay opaque.
|
|
133
160
|
function blocks(lines) {
|
|
@@ -209,7 +236,7 @@ function blocks(lines) {
|
|
|
209
236
|
return out.join('');
|
|
210
237
|
}
|
|
211
238
|
export function renderMarkdown(src, opts = {}) {
|
|
212
|
-
const
|
|
239
|
+
const withTables = opts.tables !== false;
|
|
213
240
|
const autolink = opts.autolink !== false;
|
|
214
241
|
const saved = [];
|
|
215
242
|
let s = stripAnsi(src.replace(/\r\n?/g, '\n'));
|
|
@@ -221,9 +248,8 @@ export function renderMarkdown(src, opts = {}) {
|
|
|
221
248
|
return stash(saved, `<pre${attr}><code>${body}</code></pre>`);
|
|
222
249
|
});
|
|
223
250
|
s = escapeHtml(s);
|
|
224
|
-
if (
|
|
225
|
-
s = s
|
|
226
|
-
}
|
|
251
|
+
if (withTables)
|
|
252
|
+
s = tables(s, saved, autolink);
|
|
227
253
|
s = inline(s, autolink);
|
|
228
254
|
s = blocks(s.split('\n'));
|
|
229
255
|
return restore(s, saved);
|
package/package.json
CHANGED