@vue-skuilder/common-ui 0.1.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/dist/assets/index.css +10 -0
- package/dist/common-ui.es.js +16404 -0
- package/dist/common-ui.es.js.map +1 -0
- package/dist/common-ui.umd.js +9 -0
- package/dist/common-ui.umd.js.map +1 -0
- package/dist/components/HeatMap.types.d.ts +13 -0
- package/dist/components/PaginatingToolbar.types.d.ts +40 -0
- package/dist/components/SkMouseTrap.types.d.ts +3 -0
- package/dist/components/SkMouseTrapToolTip.types.d.ts +35 -0
- package/dist/components/SnackbarService.d.ts +11 -0
- package/dist/components/StudySession.types.d.ts +6 -0
- package/dist/components/auth/index.d.ts +4 -0
- package/dist/components/cardRendering/MarkdownRendererHelpers.d.ts +22 -0
- package/dist/components/studentInputs/BaseUserInput.d.ts +16 -0
- package/dist/components/studentInputs/RadioMultipleChoice.types.d.ts +5 -0
- package/dist/composables/CompositionViewable.d.ts +33 -0
- package/dist/composables/Displayable.d.ts +47 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/index.d.ts +36 -0
- package/dist/plugins/pinia.d.ts +5 -0
- package/dist/stores/useAuthStore.d.ts +225 -0
- package/dist/stores/useCardPreviewModeStore.d.ts +8 -0
- package/dist/stores/useConfigStore.d.ts +11 -0
- package/dist/utils/SkldrMouseTrap.d.ts +32 -0
- package/package.json +67 -0
- package/src/components/HeatMap.types.ts +15 -0
- package/src/components/HeatMap.vue +354 -0
- package/src/components/PaginatingToolbar.types.ts +48 -0
- package/src/components/PaginatingToolbar.vue +75 -0
- package/src/components/SkMouseTrap.types.ts +3 -0
- package/src/components/SkMouseTrap.vue +70 -0
- package/src/components/SkMouseTrapToolTip.types.ts +41 -0
- package/src/components/SkMouseTrapToolTip.vue +316 -0
- package/src/components/SnackbarService.ts +39 -0
- package/src/components/SnackbarService.vue +71 -0
- package/src/components/StudySession.types.ts +6 -0
- package/src/components/StudySession.vue +670 -0
- package/src/components/StudySessionTimer.vue +121 -0
- package/src/components/auth/UserChip.vue +106 -0
- package/src/components/auth/UserLogin.vue +141 -0
- package/src/components/auth/UserLoginAndRegistrationContainer.vue +85 -0
- package/src/components/auth/UserRegistration.vue +181 -0
- package/src/components/auth/index.ts +4 -0
- package/src/components/cardRendering/AudioAutoPlayer.vue +131 -0
- package/src/components/cardRendering/CardLoader.vue +123 -0
- package/src/components/cardRendering/CardViewer.vue +101 -0
- package/src/components/cardRendering/CodeBlockRenderer.vue +81 -0
- package/src/components/cardRendering/MarkdownRenderer.vue +46 -0
- package/src/components/cardRendering/MarkdownRendererHelpers.ts +114 -0
- package/src/components/cardRendering/MdTokenRenderer.vue +244 -0
- package/src/components/studentInputs/BaseUserInput.ts +71 -0
- package/src/components/studentInputs/MultipleChoiceOption.vue +127 -0
- package/src/components/studentInputs/RadioMultipleChoice.types.ts +6 -0
- package/src/components/studentInputs/RadioMultipleChoice.vue +168 -0
- package/src/components/studentInputs/TrueFalse.vue +27 -0
- package/src/components/studentInputs/UserInputNumber.vue +63 -0
- package/src/components/studentInputs/UserInputString.vue +89 -0
- package/src/components/studentInputs/fillInInput.vue +71 -0
- package/src/composables/CompositionViewable.ts +180 -0
- package/src/composables/Displayable.ts +133 -0
- package/src/composables/index.ts +2 -0
- package/src/index.ts +79 -0
- package/src/plugins/pinia.ts +24 -0
- package/src/stores/useAuthStore.ts +92 -0
- package/src/stores/useCardPreviewModeStore.ts +32 -0
- package/src/stores/useConfigStore.ts +60 -0
- package/src/utils/SkldrMouseTrap.ts +141 -0
- package/src/vue-shims.d.ts +5 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-btn size="large" icon :color="playing ? 'primary lighten-3' : 'primary'" :class="{ playing }" @click="play">
|
|
3
|
+
<v-icon>mdi-volume-high</v-icon>
|
|
4
|
+
</v-btn>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { ref, onBeforeUnmount, onMounted, getCurrentInstance } from 'vue';
|
|
9
|
+
import { SkldrMouseTrap } from '../../utils/SkldrMouseTrap';
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
src: string | string[];
|
|
13
|
+
}>();
|
|
14
|
+
|
|
15
|
+
const audioElems = ref<HTMLAudioElement[]>([]);
|
|
16
|
+
const playTimeouts = ref<NodeJS.Timer[]>([]);
|
|
17
|
+
const playing = ref(false);
|
|
18
|
+
|
|
19
|
+
let staticLOCK: any = null;
|
|
20
|
+
const playbackGap = 500;
|
|
21
|
+
|
|
22
|
+
const stop = () => {
|
|
23
|
+
playing.value = false;
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
staticLOCK = null;
|
|
26
|
+
}, playbackGap);
|
|
27
|
+
|
|
28
|
+
playTimeouts.value.forEach(clearTimeout);
|
|
29
|
+
|
|
30
|
+
console.log(`Audio stopping...`);
|
|
31
|
+
audioElems.value.forEach((audio) => {
|
|
32
|
+
if (!audio.paused) {
|
|
33
|
+
audio.pause();
|
|
34
|
+
audio.currentTime = 0;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const downloadFinished = (i: number): boolean => {
|
|
40
|
+
try {
|
|
41
|
+
return !isNaN(audioElems.value[i].duration);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
throw new Error('AudioPlayer does not have an element at this index:', e);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const playByIndex = (n: number) => {
|
|
48
|
+
if (downloadFinished(n)) {
|
|
49
|
+
audioElems.value[n].play();
|
|
50
|
+
|
|
51
|
+
if (n + 1 < audioElems.value.length) {
|
|
52
|
+
const delay = (audioElems.value[n].duration + 0.7) * 1000;
|
|
53
|
+
playTimeouts.value.push(
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
if (playing.value) {
|
|
56
|
+
playByIndex(n + 1);
|
|
57
|
+
}
|
|
58
|
+
}, delay)
|
|
59
|
+
);
|
|
60
|
+
} else {
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
playing.value = false;
|
|
63
|
+
}, audioElems.value[n].duration * 1000);
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
staticLOCK = null;
|
|
66
|
+
}, audioElems.value[n].duration * 1000 + playbackGap);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
setTimeout(playByIndex, 100, n);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const play = () => {
|
|
74
|
+
if (staticLOCK === null || staticLOCK === getCurrentInstance()) {
|
|
75
|
+
staticLOCK = getCurrentInstance();
|
|
76
|
+
playing.value = true;
|
|
77
|
+
playByIndex(0);
|
|
78
|
+
} else {
|
|
79
|
+
setTimeout(play, 100);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
onMounted(() => {
|
|
84
|
+
staticLOCK = null;
|
|
85
|
+
|
|
86
|
+
if (typeof props.src === 'string') {
|
|
87
|
+
audioElems.value.push(new Audio(props.src));
|
|
88
|
+
} else {
|
|
89
|
+
props.src.forEach((url) => {
|
|
90
|
+
audioElems.value.push(new Audio(url));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Define hotkeys
|
|
95
|
+
const hotkeys = [
|
|
96
|
+
{
|
|
97
|
+
hotkey: 'up',
|
|
98
|
+
callback: play,
|
|
99
|
+
command: 'Replay Audio',
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// Add bindings
|
|
104
|
+
SkldrMouseTrap.addBinding(hotkeys);
|
|
105
|
+
|
|
106
|
+
play();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
onBeforeUnmount(() => {
|
|
110
|
+
// Clean up hotkey bindings
|
|
111
|
+
SkldrMouseTrap.removeBinding('up');
|
|
112
|
+
stop();
|
|
113
|
+
});
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<style scoped>
|
|
117
|
+
.playing {
|
|
118
|
+
/* transform: rotate(3deg) scale(1.15); */
|
|
119
|
+
animation: 0.85s ease-in-out infinite alternate pulse;
|
|
120
|
+
z-index: 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@keyframes pulse {
|
|
124
|
+
0% {
|
|
125
|
+
transform: scale(1);
|
|
126
|
+
}
|
|
127
|
+
100% {
|
|
128
|
+
transform: scale(1.15);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
</style>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<card-viewer
|
|
3
|
+
v-if="!loading"
|
|
4
|
+
class="ma-2"
|
|
5
|
+
:class="loading ? 'muted' : ''"
|
|
6
|
+
:view="view"
|
|
7
|
+
:data="data"
|
|
8
|
+
:card_id="cardID"
|
|
9
|
+
:course_id="courseID"
|
|
10
|
+
:session-order="sessionOrder"
|
|
11
|
+
@emit-response="processResponse($event)"
|
|
12
|
+
/>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
import { defineComponent, PropType } from 'vue';
|
|
17
|
+
import { getDataLayer, CardData, CardRecord, DisplayableData } from '@vue-skuilder/db';
|
|
18
|
+
import { log, displayableDataToViewData, ViewData, ViewDescriptor } from '@vue-skuilder/common';
|
|
19
|
+
import { ViewComponent } from '../../composables';
|
|
20
|
+
import CardViewer from './CardViewer.vue';
|
|
21
|
+
|
|
22
|
+
export default defineComponent({
|
|
23
|
+
name: 'CardLoader',
|
|
24
|
+
|
|
25
|
+
components: {
|
|
26
|
+
CardViewer,
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
props: {
|
|
30
|
+
sessionOrder: {
|
|
31
|
+
type: Number,
|
|
32
|
+
required: false,
|
|
33
|
+
default: 0,
|
|
34
|
+
},
|
|
35
|
+
qualified_id: {
|
|
36
|
+
type: String,
|
|
37
|
+
required: true,
|
|
38
|
+
},
|
|
39
|
+
viewLookup: {
|
|
40
|
+
type: Function as PropType<(viewDescription: ViewDescriptor | string) => ViewComponent>,
|
|
41
|
+
required: true,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
data() {
|
|
46
|
+
return {
|
|
47
|
+
loading: true,
|
|
48
|
+
view: null as ViewComponent | null,
|
|
49
|
+
data: [] as ViewData[],
|
|
50
|
+
courseID: '',
|
|
51
|
+
cardID: '',
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
created() {
|
|
56
|
+
this.loadCard();
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
methods: {
|
|
60
|
+
processResponse(r: CardRecord) {
|
|
61
|
+
log(`
|
|
62
|
+
Card was displayed at ${r.timeStamp}
|
|
63
|
+
User spent ${r.timeSpent} milliseconds with the card.
|
|
64
|
+
`);
|
|
65
|
+
this.$emit('emitResponse', r);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
async loadCard() {
|
|
69
|
+
const qualified_id = this.qualified_id;
|
|
70
|
+
console.log(`Card Loader displaying: ${qualified_id}`);
|
|
71
|
+
|
|
72
|
+
this.loading = true;
|
|
73
|
+
const _courseID = qualified_id.split('-')[0];
|
|
74
|
+
const _cardID = qualified_id.split('-')[1];
|
|
75
|
+
const courseDB = getDataLayer().getCourseDB(_courseID);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const tmpCardData = await courseDB.getCourseDoc<CardData>(_cardID);
|
|
79
|
+
const tmpView = this.viewLookup(tmpCardData.id_view);
|
|
80
|
+
const tmpDataDocs = tmpCardData.id_displayable_data.map((id) => {
|
|
81
|
+
return courseDB.getCourseDoc<DisplayableData>(id, {
|
|
82
|
+
attachments: true,
|
|
83
|
+
binary: true,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const tmpData = [];
|
|
88
|
+
|
|
89
|
+
for (const docPromise of tmpDataDocs) {
|
|
90
|
+
const doc = await docPromise;
|
|
91
|
+
tmpData.unshift(displayableDataToViewData(doc));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.data = tmpData;
|
|
95
|
+
this.view = tmpView as ViewComponent;
|
|
96
|
+
this.cardID = _cardID;
|
|
97
|
+
this.courseID = _courseID;
|
|
98
|
+
} catch (e) {
|
|
99
|
+
throw new Error(`[CardLoader] Error loading card: ${JSON.stringify(e)}, ${e}`);
|
|
100
|
+
} finally {
|
|
101
|
+
this.loading = false;
|
|
102
|
+
this.$emit('card-loaded');
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<style scoped>
|
|
110
|
+
.cardView {
|
|
111
|
+
padding: 15px;
|
|
112
|
+
border-radius: 8px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.component-fade-enter-active,
|
|
116
|
+
.component-fade-leave-active {
|
|
117
|
+
transition: opacity 0.3s ease;
|
|
118
|
+
}
|
|
119
|
+
.component-fade-enter, .component-fade-leave-to
|
|
120
|
+
/* .component-fade-leave-active below version 2.1.8 */ {
|
|
121
|
+
opacity: 0;
|
|
122
|
+
}
|
|
123
|
+
</style>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card elevation="12">
|
|
3
|
+
<transition name="component-fade" mode="out-in">
|
|
4
|
+
<component
|
|
5
|
+
:is="view"
|
|
6
|
+
ref="activeView"
|
|
7
|
+
:key="course_id + '-' + card_id + '-' + sessionOrder"
|
|
8
|
+
:data="data"
|
|
9
|
+
:modify-difficulty="user_elo.global.score - card_elo"
|
|
10
|
+
class="cardView ma-2 pa-2"
|
|
11
|
+
@emit-response="processResponse($event)"
|
|
12
|
+
/>
|
|
13
|
+
</transition>
|
|
14
|
+
</v-card>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script lang="ts">
|
|
18
|
+
import { defineComponent, PropType } from 'vue';
|
|
19
|
+
import { CardRecord } from '@vue-skuilder/db';
|
|
20
|
+
import { CourseElo, ViewData } from '@vue-skuilder/common';
|
|
21
|
+
import { ViewComponent } from '../../composables';
|
|
22
|
+
|
|
23
|
+
interface CardViewerRefs {
|
|
24
|
+
activeView: ViewComponent;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default defineComponent({
|
|
28
|
+
name: 'CardViewer',
|
|
29
|
+
|
|
30
|
+
ref: {} as CardViewerRefs,
|
|
31
|
+
|
|
32
|
+
props: {
|
|
33
|
+
sessionOrder: {
|
|
34
|
+
type: Number,
|
|
35
|
+
required: false,
|
|
36
|
+
default: 0,
|
|
37
|
+
},
|
|
38
|
+
card_id: {
|
|
39
|
+
type: String as () => PouchDB.Core.DocumentId,
|
|
40
|
+
required: true,
|
|
41
|
+
default: '',
|
|
42
|
+
},
|
|
43
|
+
course_id: {
|
|
44
|
+
type: String,
|
|
45
|
+
required: true,
|
|
46
|
+
default: '',
|
|
47
|
+
},
|
|
48
|
+
view: {
|
|
49
|
+
type: [Function, Object] as PropType<ViewComponent>,
|
|
50
|
+
required: true,
|
|
51
|
+
},
|
|
52
|
+
data: {
|
|
53
|
+
type: Array as () => ViewData[],
|
|
54
|
+
required: true,
|
|
55
|
+
},
|
|
56
|
+
user_elo: {
|
|
57
|
+
type: Object as () => CourseElo,
|
|
58
|
+
default: () => ({
|
|
59
|
+
global: {
|
|
60
|
+
score: 1000,
|
|
61
|
+
count: 0,
|
|
62
|
+
},
|
|
63
|
+
tags: {},
|
|
64
|
+
misc: {},
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
card_elo: {
|
|
68
|
+
type: Number,
|
|
69
|
+
default: 1000,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
emits: ['emitResponse'],
|
|
74
|
+
|
|
75
|
+
methods: {
|
|
76
|
+
processResponse(r: CardRecord): void {
|
|
77
|
+
console.log(`
|
|
78
|
+
Card was displayed at ${r.timeStamp}
|
|
79
|
+
User spent ${r.timeSpent} milliseconds with the card.
|
|
80
|
+
`);
|
|
81
|
+
this.$emit('emitResponse', r);
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<style scoped>
|
|
88
|
+
.cardView {
|
|
89
|
+
padding: 15px;
|
|
90
|
+
border-radius: 8px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.component-fade-enter-active,
|
|
94
|
+
.component-fade-leave-active {
|
|
95
|
+
transition: opacity 0.3s ease;
|
|
96
|
+
}
|
|
97
|
+
.component-fade-enter, .component-fade-leave-to
|
|
98
|
+
/* .component-fade-leave-active below version 2.1.8 */ {
|
|
99
|
+
opacity: 0;
|
|
100
|
+
}
|
|
101
|
+
</style>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="code-block-wrapper pa-2">
|
|
3
|
+
<div class="language-indicator" v-if="language">{{ language }}</div>
|
|
4
|
+
<highlightjs :language="language" :code="code" />
|
|
5
|
+
</div>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import hljs from 'highlight.js/lib/core';
|
|
10
|
+
import hljsVuePlugin from '@highlightjs/vue-plugin';
|
|
11
|
+
import { getCurrentInstance } from 'vue';
|
|
12
|
+
|
|
13
|
+
// Import only the languages you need
|
|
14
|
+
import javascript from 'highlight.js/lib/languages/javascript';
|
|
15
|
+
import typescript from 'highlight.js/lib/languages/typescript';
|
|
16
|
+
import css from 'highlight.js/lib/languages/css';
|
|
17
|
+
import html from 'highlight.js/lib/languages/xml';
|
|
18
|
+
import bash from 'highlight.js/lib/languages/bash';
|
|
19
|
+
import json from 'highlight.js/lib/languages/json';
|
|
20
|
+
import go from 'highlight.js/lib/languages/go';
|
|
21
|
+
import python from 'highlight.js/lib/languages/python';
|
|
22
|
+
|
|
23
|
+
// Register languages
|
|
24
|
+
hljs.registerLanguage('js', javascript);
|
|
25
|
+
hljs.registerLanguage('javascript', javascript);
|
|
26
|
+
hljs.registerLanguage('ts', typescript);
|
|
27
|
+
hljs.registerLanguage('typescript', typescript);
|
|
28
|
+
hljs.registerLanguage('css', css);
|
|
29
|
+
hljs.registerLanguage('html', html);
|
|
30
|
+
hljs.registerLanguage('bash', bash);
|
|
31
|
+
hljs.registerLanguage('sh', bash);
|
|
32
|
+
hljs.registerLanguage('json', json);
|
|
33
|
+
hljs.registerLanguage('go', go);
|
|
34
|
+
hljs.registerLanguage('golang', go);
|
|
35
|
+
hljs.registerLanguage('python', python);
|
|
36
|
+
|
|
37
|
+
// Get access to the current component instance
|
|
38
|
+
const instance = getCurrentInstance();
|
|
39
|
+
if (instance) {
|
|
40
|
+
// Register the component locally
|
|
41
|
+
instance.appContext.app.component('highlightjs', hljsVuePlugin.component);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
defineProps({
|
|
45
|
+
code: {
|
|
46
|
+
type: String,
|
|
47
|
+
required: true,
|
|
48
|
+
},
|
|
49
|
+
language: {
|
|
50
|
+
type: String,
|
|
51
|
+
default: '',
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<style>
|
|
57
|
+
/* Import a highlight.js theme */
|
|
58
|
+
@import 'highlight.js/styles/github.css';
|
|
59
|
+
|
|
60
|
+
.code-block-wrapper {
|
|
61
|
+
margin: 1rem 0;
|
|
62
|
+
border-radius: 4px;
|
|
63
|
+
background-color: #f6f8fa;
|
|
64
|
+
overflow: auto;
|
|
65
|
+
position: relative;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.language-indicator {
|
|
69
|
+
position: absolute;
|
|
70
|
+
top: 8px;
|
|
71
|
+
right: 10px;
|
|
72
|
+
padding: 2px 8px;
|
|
73
|
+
font-size: 0.75rem;
|
|
74
|
+
color: #666;
|
|
75
|
+
background-color: rgba(255, 255, 255, 0.7);
|
|
76
|
+
border-radius: 4px;
|
|
77
|
+
z-index: 1;
|
|
78
|
+
font-family: monospace;
|
|
79
|
+
text-transform: lowercase;
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<!-- <v-textarea v-if="testRoute" v-model="md" variant="outlined" label="Type Here" name="name" /> -->
|
|
4
|
+
<span v-for="(token, i) in tokens" :key="i">
|
|
5
|
+
<md-token-renderer v-if="token.type" :token="token" :last="i === tokens.length - 1" />
|
|
6
|
+
<audio-auto-player v-else-if="token.audio" :src="token.audio" />
|
|
7
|
+
<!-- // [ ] insert img display here. "if token.image ? (also see if the audio aboke is functional)" -->
|
|
8
|
+
</span>
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
import { defineComponent, PropType } from 'vue';
|
|
14
|
+
import MdTokenRenderer from './MdTokenRenderer.vue';
|
|
15
|
+
import AudioAutoPlayer from './AudioAutoPlayer.vue';
|
|
16
|
+
import * as marked from 'marked';
|
|
17
|
+
|
|
18
|
+
type SkldrToken =
|
|
19
|
+
| marked.Token
|
|
20
|
+
| {
|
|
21
|
+
type: false;
|
|
22
|
+
audio: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Using Options API with mixin
|
|
26
|
+
export default defineComponent({
|
|
27
|
+
name: 'MarkdownRenderer',
|
|
28
|
+
components: {
|
|
29
|
+
MdTokenRenderer,
|
|
30
|
+
AudioAutoPlayer,
|
|
31
|
+
},
|
|
32
|
+
props: {
|
|
33
|
+
md: {
|
|
34
|
+
type: String as PropType<string>,
|
|
35
|
+
required: true,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
computed: {
|
|
39
|
+
tokens(): SkldrToken[] {
|
|
40
|
+
return marked.lexer(this.md);
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<style lang="css" scoped></style>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Token, MarkedToken, Tokens } from 'marked';
|
|
2
|
+
import * as marked from 'marked';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* recursively splits text according to the passed delimiters.
|
|
6
|
+
*
|
|
7
|
+
* eg: ("abcde", "b", "d") => ["a", "bcd", "e"]
|
|
8
|
+
* ("a[b][c]", "[", "]") => ["a", "[b]", "[c]"]
|
|
9
|
+
*
|
|
10
|
+
* it does not check that the delimiters are well formed in the text
|
|
11
|
+
* @param text the text to be split
|
|
12
|
+
* @param l the left delimiter
|
|
13
|
+
* @param r the right delimiter
|
|
14
|
+
* @returns the split result
|
|
15
|
+
*/
|
|
16
|
+
export function splitByDelimiters(text: string, l: string, r: string): string[] {
|
|
17
|
+
if (text.length === 0) return [];
|
|
18
|
+
|
|
19
|
+
let ret: string[] = [];
|
|
20
|
+
|
|
21
|
+
const left = text.indexOf(l);
|
|
22
|
+
const right = text.indexOf(r, left);
|
|
23
|
+
|
|
24
|
+
if (left >= 0 && right > left) {
|
|
25
|
+
// pre-delimited characters
|
|
26
|
+
ret.push(text.substring(0, left));
|
|
27
|
+
// delimited section
|
|
28
|
+
ret.push(text.substring(left, right + r.length));
|
|
29
|
+
// recurse on remaining text
|
|
30
|
+
ret = ret.concat(splitByDelimiters(text.substring(right + r.length), l, r));
|
|
31
|
+
} else {
|
|
32
|
+
return [text];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return ret;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function splitTextToken(token: Tokens.Text): Tokens.Text[] {
|
|
39
|
+
if (containsComponent(token)) {
|
|
40
|
+
const textChunks = splitByDelimiters(token.text, '{{', '}}');
|
|
41
|
+
const rawChunks = splitByDelimiters(token.raw, '{{', '}}');
|
|
42
|
+
|
|
43
|
+
if (textChunks.length === rawChunks.length) {
|
|
44
|
+
return textChunks.map((c, i) => {
|
|
45
|
+
return {
|
|
46
|
+
type: 'text',
|
|
47
|
+
text: textChunks[i],
|
|
48
|
+
raw: rawChunks[i],
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
} else {
|
|
52
|
+
throw new Error(`Error parsing markdown`);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
return [token];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type TokenOrComponent = MarkedToken | { type: 'component'; raw: string };
|
|
60
|
+
|
|
61
|
+
export function splitParagraphToken(token: Tokens.Paragraph): TokenOrComponent[] {
|
|
62
|
+
let ret: MarkedToken[] = [];
|
|
63
|
+
|
|
64
|
+
if (containsComponent(token)) {
|
|
65
|
+
const textChunks = splitByDelimiters(token.text, '{{', '}}');
|
|
66
|
+
const rawChunks = splitByDelimiters(token.raw, '{{', '}}');
|
|
67
|
+
if (textChunks.length === rawChunks.length) {
|
|
68
|
+
for (let i = 0; i < textChunks.length; i++) {
|
|
69
|
+
const textToken = {
|
|
70
|
+
type: 'text',
|
|
71
|
+
text: textChunks[i],
|
|
72
|
+
raw: rawChunks[i],
|
|
73
|
+
} as Tokens.Text;
|
|
74
|
+
|
|
75
|
+
if (isComponent(textToken)) {
|
|
76
|
+
ret.push(textToken);
|
|
77
|
+
} else {
|
|
78
|
+
marked.lexer(rawChunks[i]).forEach((t) => {
|
|
79
|
+
if (t.type === 'paragraph') {
|
|
80
|
+
ret = ret.concat(t.tokens as MarkedToken[]);
|
|
81
|
+
} else {
|
|
82
|
+
ret.push(t as MarkedToken);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return ret;
|
|
88
|
+
} else {
|
|
89
|
+
throw new Error(`Error parsing Markdown`);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
ret.push(token);
|
|
93
|
+
}
|
|
94
|
+
return ret;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function containsComponent(token: MarkedToken) {
|
|
98
|
+
if (token.type === 'text' || token.type === 'paragraph') {
|
|
99
|
+
const opening = token.raw.indexOf('{{');
|
|
100
|
+
const closing = token.raw.indexOf('}}');
|
|
101
|
+
|
|
102
|
+
if (opening !== -1 && closing !== -1 && closing > opening) {
|
|
103
|
+
return true;
|
|
104
|
+
} else {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function isComponent(token: MarkedToken) {
|
|
113
|
+
return token.type === 'text' && token.text.startsWith('{{') && token.text.endsWith('}}');
|
|
114
|
+
}
|