nuxt-ui-basekit 0.2.0 → 0.2.1
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/app/assets/css/basekit.css +23 -24
- package/app/components/BaseKitBackLink.vue +9 -9
- package/app/components/BaseKitChoiceCard.vue +19 -19
- package/app/components/BaseKitConfirmModal.vue +7 -7
- package/app/components/BaseKitDataTable.vue +30 -30
- package/app/components/BaseKitEmptyState.vue +7 -7
- package/app/components/BaseKitFileUpload.vue +5 -5
- package/app/components/BaseKitIconPicker.vue +8 -8
- package/app/components/BaseKitMarkdownEditor.vue +11 -9
- package/app/components/BaseKitPending.vue +15 -16
- package/app/components/BaseKitRecordPicker.vue +14 -14
- package/app/components/BaseKitSettingRow.vue +15 -14
- package/app/components/BaseKitStatTile.vue +8 -8
- package/app/components/BaseKitTabs.vue +33 -34
- package/app/components/BaseKitViewLink.vue +14 -14
- package/app/components/charts/BaseKitChartBars.vue +8 -9
- package/app/components/charts/BaseKitChartColumns.vue +30 -31
- package/app/components/charts/BaseKitChartDonut.vue +18 -18
- package/app/components/charts/BaseKitChartFigure.vue +10 -10
- package/app/components/charts/BaseKitChartMeter.vue +4 -4
- package/app/composables/useBaseKit.ts +36 -41
- package/app/composables/useChartPalette.ts +14 -14
- package/app/composables/useChartWidth.ts +10 -10
- package/app/composables/useConfirm.ts +3 -3
- package/app/stores/confirm.ts +16 -16
- package/app/utils/html-to-markdown.ts +14 -16
- package/app/utils/markdown.ts +8 -7
- package/package.json +1 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* The frame around a chart: card, title, legend, and the switch to the table
|
|
4
|
+
* view.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* That switch is not a nicety. A chart encodes values through colour and
|
|
7
|
+
* length; anyone who gets nothing out of that — a colour vision deficiency, a
|
|
8
|
+
* screen reader, a printout — needs the same numbers in readable form. Which
|
|
9
|
+
* is why every card here brings its table along, and why the switch is visible
|
|
10
|
+
* rather than buried in a menu.
|
|
11
11
|
*/
|
|
12
12
|
import { computed, ref } from 'vue'
|
|
13
13
|
import { useBaseKitLabels } from '../../composables/useBaseKit'
|
|
@@ -15,11 +15,11 @@ import { useBaseKitLabels } from '../../composables/useBaseKit'
|
|
|
15
15
|
const props = withDefaults(defineProps<{
|
|
16
16
|
title: string
|
|
17
17
|
subtitle?: string
|
|
18
|
-
/**
|
|
18
|
+
/** Where the card leads. Without a target the title stays a title. */
|
|
19
19
|
href?: string
|
|
20
|
-
/**
|
|
20
|
+
/** Grey the chart out while something reloads in the background. */
|
|
21
21
|
stale?: boolean
|
|
22
|
-
/**
|
|
22
|
+
/** Without a table slot the switch disappears. */
|
|
23
23
|
tableAvailable?: boolean
|
|
24
24
|
}>(), {
|
|
25
25
|
subtitle: undefined,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* A ratio against a known limit — this many of that many.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* A meter presupposes that a whole exists. Where none does — bytes without a
|
|
6
|
+
* quota, a count without a ceiling — the number belongs in a tile and not
|
|
7
|
+
* here; a full bar would otherwise claim a limit had been reached.
|
|
8
8
|
*/
|
|
9
9
|
import { computed } from 'vue'
|
|
10
10
|
import { baseKitChartColor, useChartFormat } from '../../composables/useChartPalette'
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { computed, inject, type ComputedRef, type InjectionKey } from 'vue'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* die sie einsetzt.
|
|
4
|
+
* The only wire between the BaseKit components and the application using them.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* The components are meant to run in any Nuxt project. That rules out calling
|
|
7
|
+
* `vue-i18n` and it rules out knowing translation keys: `t('common.search')`
|
|
8
|
+
* exists only in the one application that carries that key, and a package
|
|
9
|
+
* which presupposes such keys is not a package.
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Instead the application hands language and labels in once, from a plugin
|
|
12
|
+
* that pulls the values out of its own source:
|
|
14
13
|
*
|
|
15
14
|
* // plugins/basekit.ts
|
|
16
15
|
* export default defineNuxtPlugin((nuxtApp) => {
|
|
@@ -21,39 +20,35 @@ import { computed, inject, type ComputedRef, type InjectionKey } from 'vue'
|
|
|
21
20
|
* nuxtApp.vueApp.provide(baseKitKey, config)
|
|
22
21
|
* })
|
|
23
22
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Provide nothing and the defaults below apply. One thing worth knowing when
|
|
24
|
+
* the values come from an i18n library: `useI18n()` requires a component setup
|
|
25
|
+
* context and throws inside a plugin. Take the instance from `nuxtApp.$i18n`,
|
|
26
|
+
* and only when the `computed` is read.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* `nuxtApp.$i18n` geholt, und zwar erst beim Lesen des `computed`.
|
|
28
|
+
* Over `provide`/`inject` rather than `useNuxtApp()`: that way the components
|
|
29
|
+
* also run in a Vitest mount without a Nuxt context, and two requests share
|
|
30
|
+
* nothing under SSR.
|
|
32
31
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* teilen sich zwei Anfragen nichts.
|
|
36
|
-
*
|
|
37
|
-
* Einzelne Beschriftungen bleiben weiterhin als Prop überschreibbar — hier
|
|
38
|
-
* steht nur, was ohne Zutun herauskommt.
|
|
32
|
+
* Individual labels stay overridable as props — what follows only decides what
|
|
33
|
+
* comes out when nothing is passed.
|
|
39
34
|
*/
|
|
40
35
|
export interface BaseKitLabels {
|
|
41
|
-
/**
|
|
36
|
+
/** Search field above lists and pickers. */
|
|
42
37
|
search: string
|
|
43
|
-
/** Filter
|
|
38
|
+
/** Filter entry for "no restriction". */
|
|
44
39
|
all: string
|
|
45
40
|
select: string
|
|
46
41
|
change: string
|
|
47
42
|
edit: string
|
|
48
43
|
cancel: string
|
|
49
44
|
confirm: string
|
|
50
|
-
/**
|
|
45
|
+
/** Heading of the confirmation when the caller passes none. */
|
|
51
46
|
confirmTitle: string
|
|
52
|
-
/**
|
|
47
|
+
/** Body of the confirmation when the caller passes none. */
|
|
53
48
|
confirmBody: string
|
|
54
|
-
/**
|
|
49
|
+
/** Empty list — nothing exists yet. */
|
|
55
50
|
empty: string
|
|
56
|
-
/**
|
|
51
|
+
/** Empty list — the search ran and found nothing. */
|
|
57
52
|
noResults: string
|
|
58
53
|
noResultsHint: string
|
|
59
54
|
back: string
|
|
@@ -62,15 +57,15 @@ export interface BaseKitLabels {
|
|
|
62
57
|
perPage: string
|
|
63
58
|
iconChoose: string
|
|
64
59
|
iconEmpty: string
|
|
65
|
-
/**
|
|
60
|
+
/** Clearing the choice — the empty option in the icon picker. */
|
|
66
61
|
iconClear: string
|
|
67
62
|
chartAsTable: string
|
|
68
63
|
chartAsChart: string
|
|
69
|
-
/**
|
|
64
|
+
/** "1–25 of 300" — a function, because the numbers sit inside the sentence. */
|
|
70
65
|
paginationRange: (range: { from: number, to: number, total: number }) => string
|
|
71
66
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
67
|
+
* Toolbar of the Markdown editor. Its own group, because the terms occur
|
|
68
|
+
* nowhere else and the top level would otherwise be half made of them.
|
|
74
69
|
*/
|
|
75
70
|
markdown: {
|
|
76
71
|
bold: string
|
|
@@ -82,24 +77,24 @@ export interface BaseKitLabels {
|
|
|
82
77
|
quote: string
|
|
83
78
|
code: string
|
|
84
79
|
link: string
|
|
85
|
-
/**
|
|
80
|
+
/** Prompt shown when setting a link. */
|
|
86
81
|
linkPrompt: string
|
|
87
82
|
}
|
|
88
83
|
}
|
|
89
84
|
|
|
90
85
|
export interface BaseKitConfig {
|
|
91
86
|
/**
|
|
92
|
-
* BCP-47
|
|
93
|
-
*
|
|
87
|
+
* BCP-47 tag for `Intl` — drives thousands separators, percentages and date
|
|
88
|
+
* formats in the charts.
|
|
94
89
|
*/
|
|
95
90
|
locale: string
|
|
96
91
|
labels: BaseKitLabels
|
|
97
92
|
}
|
|
98
93
|
|
|
99
94
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
95
|
+
* Defaults: English. The package cannot know what language the application
|
|
96
|
+
* runs in, and English is the one that shuts out the fewest readers. Anything
|
|
97
|
+
* else is handed in through the plugin above.
|
|
103
98
|
*/
|
|
104
99
|
export const BASEKIT_DEFAULTS: BaseKitConfig = {
|
|
105
100
|
locale: 'en-US',
|
|
@@ -144,16 +139,16 @@ export const BASEKIT_DEFAULTS: BaseKitConfig = {
|
|
|
144
139
|
export const baseKitKey: InjectionKey<ComputedRef<BaseKitConfig>> = Symbol('basekit')
|
|
145
140
|
|
|
146
141
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
142
|
+
* Configuration for a BaseKit component. With nothing provided the defaults
|
|
143
|
+
* apply — the component then renders in English instead of showing empty
|
|
144
|
+
* labels.
|
|
150
145
|
*/
|
|
151
146
|
export function useBaseKit(): ComputedRef<BaseKitConfig> {
|
|
152
147
|
const provided = inject(baseKitKey, null)
|
|
153
148
|
return provided ?? computed(() => BASEKIT_DEFAULTS)
|
|
154
149
|
}
|
|
155
150
|
|
|
156
|
-
/**
|
|
151
|
+
/** Short form for the common case of only needing the labels. */
|
|
157
152
|
export function useBaseKitLabels(): ComputedRef<BaseKitLabels> {
|
|
158
153
|
const config = useBaseKit()
|
|
159
154
|
return computed(() => config.value.labels)
|
|
@@ -2,17 +2,17 @@ import { computed } from 'vue'
|
|
|
2
2
|
import { useBaseKit } from './useBaseKit'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Series colours and number formats for the `BaseKitChart*` components.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* The colours live as CSS variables in `app/assets/css/basekit.css` and are
|
|
8
|
+
* validated as a **set**, order included. That is why there is lookup by index
|
|
9
|
+
* here and no generation: a sixth colour would be indistinguishable from one
|
|
10
|
+
* of the five under a colour vision deficiency. More series than that means
|
|
11
|
+
* grouping them.
|
|
12
12
|
*/
|
|
13
13
|
export const BASEKIT_CHART_SLOTS = 5
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/** Colour for series `index`, zero-based. From slot 6 on, the muted grey. */
|
|
16
16
|
export function baseKitChartColor(index: number): string {
|
|
17
17
|
return index < BASEKIT_CHART_SLOTS
|
|
18
18
|
? `var(--basekit-chart-${index + 1})`
|
|
@@ -33,7 +33,7 @@ export function useChartFormat(): {
|
|
|
33
33
|
return new Intl.NumberFormat(locale.value).format(value)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/**
|
|
36
|
+
/** For values on ticks and in tiles: 12,400 becomes 12.4K. */
|
|
37
37
|
function compact(value: number): string {
|
|
38
38
|
return value < 10000
|
|
39
39
|
? number(value)
|
|
@@ -41,9 +41,9 @@ export function useChartFormat(): {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* A share in percent, rounded down as long as the whole has not been
|
|
45
|
+
* reached — otherwise 9,999 out of 10,000 would pass as "100%" and the one
|
|
46
|
+
* missing item would be rounded out of sight.
|
|
47
47
|
*/
|
|
48
48
|
function percent(value: number, total: number): string {
|
|
49
49
|
if (total <= 0) return '—'
|
|
@@ -61,8 +61,8 @@ export function useChartFormat(): {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
64
|
+
* Decimal prefixes (kB, MB, GB), not KiB/MiB. Storage is billed that way,
|
|
65
|
+
* and the number here should match the one on the invoice.
|
|
66
66
|
*/
|
|
67
67
|
function bytes(value: number): string {
|
|
68
68
|
const units = ['B', 'kB', 'MB', 'GB', 'TB']
|
|
@@ -76,7 +76,7 @@ export function useChartFormat(): {
|
|
|
76
76
|
return `${new Intl.NumberFormat(locale.value, { maximumFractionDigits: digits }).format(size)} ${units[unit]}`
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/**
|
|
79
|
+
/** Runtime as "14 h 20 min"; below an hour, minutes only. */
|
|
80
80
|
function duration(seconds: number): string {
|
|
81
81
|
const total = Math.max(0, Math.round(seconds / 60))
|
|
82
82
|
const hours = Math.floor(total / 60)
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { onBeforeUnmount, onMounted, ref, type Ref } from 'vue'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Measures a container so an SVG can draw in real pixels instead of going
|
|
5
|
+
* through a scaled `viewBox`.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* The difference is the type: a `viewBox` scaled up to container width drags
|
|
8
|
+
* axis labels and values up with it — a wide card then shows 15px text where
|
|
9
|
+
* 11 was meant. With a measured width, text stays text and only the geometry
|
|
10
|
+
* grows.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Before mount (SSR, first frame) `fallback` applies. The chart is therefore
|
|
13
|
+
* there straight away and settles once, rather than flickering.
|
|
14
14
|
*/
|
|
15
15
|
export function useChartWidth(fallback = 640): {
|
|
16
16
|
el: Ref<HTMLElement | null>
|
|
@@ -25,8 +25,8 @@ export function useChartWidth(fallback = 640): {
|
|
|
25
25
|
|
|
26
26
|
observer = new ResizeObserver((entries) => {
|
|
27
27
|
const measured = entries[0]?.contentRect.width ?? 0
|
|
28
|
-
//
|
|
29
|
-
//
|
|
28
|
+
// Below 240px the geometry becomes useless; draw narrow and let the
|
|
29
|
+
// container scroll instead.
|
|
30
30
|
if (measured > 0) width.value = Math.max(240, Math.round(measured))
|
|
31
31
|
})
|
|
32
32
|
observer.observe(el.value)
|
|
@@ -2,10 +2,10 @@ import { useConfirmStore } from '../stores/confirm'
|
|
|
2
2
|
import type { BaseKitConfirmOptions } from '../stores/confirm'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* A confirmation prompt in place of the native `window.confirm`.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Opens the globally mounted `BaseKitConfirmModal` and returns a promise that
|
|
8
|
+
* resolves to `true` or `false`.
|
|
9
9
|
*
|
|
10
10
|
* const confirm = useConfirm()
|
|
11
11
|
* if (!(await confirm({ description: t('…'), color: 'error', confirmLabel: t('common.delete') }))) return
|
package/app/stores/confirm.ts
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/** Options for one confirmation prompt. */
|
|
5
5
|
export interface BaseKitConfirmOptions {
|
|
6
|
-
/**
|
|
6
|
+
/** Dialog title. Falls back to the `confirmTitle` label. */
|
|
7
7
|
title?: string
|
|
8
|
-
/**
|
|
8
|
+
/** Explanation or consequence — spell it out for anything destructive. */
|
|
9
9
|
description?: string
|
|
10
|
-
/**
|
|
10
|
+
/** Label of the confirm button. Falls back to the `confirm` label. */
|
|
11
11
|
confirmLabel?: string
|
|
12
|
-
/**
|
|
12
|
+
/** Label of the cancel button. Falls back to the `cancel` label. */
|
|
13
13
|
cancelLabel?: string
|
|
14
|
-
/**
|
|
14
|
+
/** Colour of the confirm button — `error` for destructive actions. */
|
|
15
15
|
color?: 'error' | 'primary'
|
|
16
|
-
/**
|
|
16
|
+
/** Optional icon on the confirm button. */
|
|
17
17
|
icon?: string
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* The confirmation store — replaces native `window.confirm` dialogs with a
|
|
22
|
+
* themed, dark-mode-capable prompt.
|
|
23
23
|
*
|
|
24
|
-
* `ask()`
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* `ask()` opens the globally mounted `BaseKitConfirmModal` and returns a
|
|
25
|
+
* promise resolving to `true` (confirmed) or `false` (cancelled or closed).
|
|
26
|
+
* The resolver lives outside reactivity so exactly one answer goes back per
|
|
27
|
+
* prompt.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
29
|
+
* Used through the `useConfirm()` composable:
|
|
30
30
|
* const confirm = useConfirm()
|
|
31
31
|
* if (!(await confirm({ description: t('…'), color: 'error' }))) return
|
|
32
32
|
*/
|
|
@@ -36,7 +36,7 @@ export const useConfirmStore = defineStore('basekit:confirm', () => {
|
|
|
36
36
|
let resolver: ((value: boolean) => void) | null = null
|
|
37
37
|
|
|
38
38
|
function ask(opts: BaseKitConfirmOptions = {}): Promise<boolean> {
|
|
39
|
-
//
|
|
39
|
+
// A prompt still running is resolved as cancelled.
|
|
40
40
|
resolver?.(false)
|
|
41
41
|
options.value = opts
|
|
42
42
|
open.value = true
|
|
@@ -45,7 +45,7 @@ export const useConfirmStore = defineStore('basekit:confirm', () => {
|
|
|
45
45
|
})
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/**
|
|
48
|
+
/** Answer the prompt and close the dialog. */
|
|
49
49
|
function settle(value: boolean): void {
|
|
50
50
|
open.value = false
|
|
51
51
|
resolver?.(value)
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HTML
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* - Tiptap speichert HTML → hier nach Markdown serialisieren.
|
|
6
|
-
* - Legacy-Text-Blöcke mit rohem `config.html` einmalig nach Markdown wandeln.
|
|
2
|
+
* HTML to Markdown, through turndown. Kept apart from `markdown.ts` on
|
|
3
|
+
* purpose: that one may end up in a public bundle and should not drag turndown
|
|
4
|
+
* along, while this one is only ever needed where something is edited.
|
|
7
5
|
*
|
|
8
|
-
* **turndown
|
|
9
|
-
* CommonJS;
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* **turndown is loaded lazily, and that is deliberate.** The package is
|
|
7
|
+
* CommonJS; an import at module level lands in the server bundle and throws
|
|
8
|
+
* "require is not defined in ES module scope" while rendering. The editor runs
|
|
9
|
+
* in the browser anyway — on the server this function is never called.
|
|
12
10
|
*/
|
|
13
11
|
type Turndown = { turndown: (html: string) => string }
|
|
14
12
|
|
|
@@ -18,14 +16,14 @@ function service(): Turndown | null {
|
|
|
18
16
|
if (instance) return instance
|
|
19
17
|
if (import.meta.server) return null
|
|
20
18
|
|
|
21
|
-
//
|
|
22
|
-
// `prepare()`
|
|
19
|
+
// Synchronous access to an already loaded module: the editor calls
|
|
20
|
+
// `prepare()` on mount, well before anything can be saved.
|
|
23
21
|
return instance
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* Preloads turndown in the browser. The editor components call this on mount
|
|
26
|
+
* so {@link htmlToMarkdown} can stay synchronous afterwards.
|
|
29
27
|
*/
|
|
30
28
|
export async function prepareHtmlToMarkdown(): Promise<void> {
|
|
31
29
|
if (instance || import.meta.server) return
|
|
@@ -40,8 +38,8 @@ export async function prepareHtmlToMarkdown(): Promise<void> {
|
|
|
40
38
|
export function htmlToMarkdown(html?: string | null): string {
|
|
41
39
|
if (!html) return ''
|
|
42
40
|
const td = service()
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
41
|
+
// Without turndown loaded — on the server, or when prepare() was forgotten
|
|
42
|
+
// — return the original rather than throwing: the text would otherwise be
|
|
43
|
+
// lost on save.
|
|
46
44
|
return td ? td.turndown(String(html)).trim() : String(html)
|
|
47
45
|
}
|
package/app/utils/markdown.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import MarkdownIt from 'markdown-it'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Markdown
|
|
4
|
+
* Markdown to HTML.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* `html: false` on purpose: raw HTML inside the Markdown is NOT passed
|
|
7
|
+
* through. That closes the biggest XSS hole without a separate sanitizer —
|
|
8
|
+
* markdown-it also blocks `javascript:` links through its default
|
|
9
|
+
* `validateLink`. `linkify` makes bare URLs clickable, `typographer` smooths
|
|
10
|
+
* quotes and dashes.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* Shared between display and editor preview, so that editing and output look
|
|
13
|
+
* the same.
|
|
13
14
|
*/
|
|
14
15
|
const md: MarkdownIt = new MarkdownIt({
|
|
15
16
|
html: false,
|
package/package.json
CHANGED