Skip to content

Commit c208871

Browse files
feat(snippets): add markdown preview (#15)
1 parent ed94588 commit c208871

File tree

10 files changed

+12668
-9
lines changed

10 files changed

+12668
-9
lines changed

package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,14 @@
3939
"axios": "^0.26.1",
4040
"electron-store": "^8.0.1",
4141
"fs-extra": "^10.0.1",
42+
"highlight.js": "^11.5.1",
4243
"lowdb": "^3.0.0",
44+
"markdown-it": "^12.3.2",
45+
"markdown-it-link-attributes": "^4.0.0",
4346
"mitt": "^3.0.0",
4447
"nanoid": "^3.3.1",
4548
"pinia": "^2.0.12",
49+
"sanitize-html": "^2.7.0",
4650
"universal-analytics": "^0.5.3",
4751
"vue": "^3.2.26",
4852
"vue-router": "^4.0.12",
@@ -55,7 +59,10 @@
5559
"@types/ace": "^0.0.48",
5660
"@types/estree": "^0.0.51",
5761
"@types/lowdb": "^1.0.11",
62+
"@types/markdown-it": "^12.2.3",
63+
"@types/markdown-it-link-attributes": "^3.0.1",
5864
"@types/node": "^17.0.4",
65+
"@types/sanitize-html": "^2.6.2",
5966
"@types/webpack": "^5.28.0",
6067
"@typescript-eslint/eslint-plugin": "^5.8.0",
6168
"@typescript-eslint/parser": "^5.8.0",

src/renderer/App.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ import { ref, watch } from 'vue'
1919
import { ipc } from './electron'
2020
import { useAppStore } from './store/app'
2121
import { repository } from '../../package.json'
22+
import { useSnippetStore } from './store/snippets'
2223
2324
// По какой то причине необходимо явно установить роут в '/'
2425
// для корректного поведения в продакшен сборке
2526
// TODO: выяснить причину
2627
router.push('/')
2728
2829
const appStore = useAppStore()
30+
const snippetStore = useSnippetStore()
2931
3032
const isUpdateAvailable = ref(false)
3133
@@ -43,6 +45,13 @@ watch(
4345
{ immediate: true }
4446
)
4547
48+
watch(
49+
() => snippetStore.selectedId,
50+
() => {
51+
snippetStore.isMarkdownPreview = false
52+
}
53+
)
54+
4655
ipc.on('main-menu:preferences', () => {
4756
router.push('/preferences')
4857
})

src/renderer/components/editor/TheEditor.vue

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ interface Props {
4242
lang: Language
4343
theme: string
4444
fragments: boolean
45+
fragmentIndex: number
46+
snippetId: string
4547
modelValue: string
48+
isSearchMode: boolean
4649
}
4750
4851
interface Emits {
@@ -151,6 +154,15 @@ const setTheme = () => {
151154
editor.session.setMode(`ace/theme/${props.theme}`)
152155
}
153156
157+
const resetUndoStack = () => {
158+
editor.getSession().setUndoManager(new ace.UndoManager())
159+
}
160+
161+
const setCursorToStartAndClearSelection = () => {
162+
editor.moveCursorTo(0, 0)
163+
editor.clearSelection()
164+
}
165+
154166
const findAll = (q: string) => {
155167
if (q === '') return
156168
editor.findAll(q, { caseSensitive: false, preventScroll: true })
@@ -182,6 +194,16 @@ watch(
182194
}
183195
)
184196
197+
watch(
198+
() => [props.snippetId, props.fragmentIndex],
199+
() => {
200+
resetUndoStack()
201+
if (!props.isSearchMode) {
202+
setCursorToStartAndClearSelection()
203+
}
204+
}
205+
)
206+
185207
window.addEventListener('resize', () => {
186208
forceRefresh.value = Math.random()
187209
})
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<template>
2+
<div class="markdown markdown-github">
3+
<PerfectScrollbar>
4+
<div v-html="renderer" />
5+
</PerfectScrollbar>
6+
</div>
7+
</template>
8+
9+
<script setup lang="ts">
10+
import { useAppStore } from '@/store/app'
11+
import { useSnippetStore } from '@/store/snippets'
12+
import MarkdownIt from 'markdown-it'
13+
import sanitizeHtml from 'sanitize-html'
14+
import hljs from 'highlight.js'
15+
import mila from 'markdown-it-link-attributes'
16+
import 'highlight.js/styles/github.css'
17+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
18+
import { ipc } from '@/electron'
19+
20+
interface Props {
21+
value: string
22+
}
23+
24+
const props = defineProps<Props>()
25+
26+
const appStore = useAppStore()
27+
const snippetStore = useSnippetStore()
28+
29+
let md: MarkdownIt
30+
const forceRefresh = ref()
31+
32+
const init = () => {
33+
md = new MarkdownIt({
34+
html: true,
35+
langPrefix: 'language-',
36+
highlight (str, lang) {
37+
if (lang && hljs.getLanguage(lang)) {
38+
try {
39+
return `<pre class="hljs"><code>${
40+
hljs.highlight(str, {
41+
language: lang,
42+
ignoreIllegals: true
43+
}).value
44+
}</code></pre>`
45+
} catch (err) {
46+
console.log(err)
47+
}
48+
}
49+
return `<pre class="hljs"><code>${MarkdownIt().utils.escapeHtml(
50+
str
51+
)}</code></pre>`
52+
}
53+
})
54+
55+
md.use(mila, {
56+
attrs: {
57+
class: 'external'
58+
}
59+
})
60+
}
61+
62+
const getRenderer = () => {
63+
const raw = md?.render(props.value)
64+
const html = sanitizeHtml(raw, {
65+
allowedTags: false,
66+
allowedAttributes: {
67+
'*': [
68+
'align',
69+
'alt',
70+
'height',
71+
'href',
72+
'name',
73+
'src',
74+
'target',
75+
'width',
76+
'class'
77+
]
78+
}
79+
})
80+
return html
81+
}
82+
83+
const renderer = computed(() => getRenderer())
84+
85+
const openExternal = (e: Event) => {
86+
const el = e.target as HTMLAnchorElement
87+
if (el.classList.contains('external')) {
88+
e.preventDefault()
89+
ipc.invoke('main:open-url', el.href)
90+
}
91+
}
92+
93+
const height = computed(() => {
94+
// eslint-disable-next-line no-unused-expressions
95+
forceRefresh.value
96+
97+
let result =
98+
appStore.sizes.editor.titleHeight +
99+
appStore.sizes.titlebar +
100+
appStore.sizes.editor.footerHeight
101+
102+
if (snippetStore.isFragmentsShow) {
103+
result += appStore.sizes.editor.fragmentsHeight
104+
}
105+
106+
if (snippetStore.isTagsShow) {
107+
result += appStore.sizes.editor.tagsHeight
108+
}
109+
110+
return window.innerHeight - result + 'px'
111+
})
112+
113+
init()
114+
115+
onMounted(() => {
116+
document.addEventListener('click', openExternal)
117+
})
118+
119+
onBeforeUnmount(() => {
120+
document.removeEventListener('click', openExternal)
121+
})
122+
123+
window.addEventListener('resize', () => {
124+
forceRefresh.value = Math.random()
125+
})
126+
</script>
127+
128+
<style lang="scss" scoped>
129+
.markdown {
130+
padding: 0 var(--spacing-xs);
131+
:deep(h1, h2, h3, h4, h5, h6) {
132+
&:first-child {
133+
margin-top: 0;
134+
}
135+
}
136+
:deep(.ps) {
137+
height: v-bind(height);
138+
}
139+
}
140+
</style>

src/renderer/components/snippets/SnippetHeader.vue

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,18 @@
1010
>
1111
</div>
1212
<div class="action">
13-
<AppActionButton>
14-
<UniconsArrow @click="onCopySnippet" />
13+
<AppActionButton
14+
v-if="snippetStore.currentLanguage === 'markdown'"
15+
@click="onClickMarkdownPreview"
16+
>
17+
<UniconsEye v-if="!snippetStore.isMarkdownPreview" />
18+
<UniconsEyeSlash v-else />
19+
</AppActionButton>
20+
<AppActionButton @click="onCopySnippet">
21+
<UniconsArrow />
1522
</AppActionButton>
16-
<AppActionButton>
17-
<UniconsPlus @click="onAddNewFragment" />
23+
<AppActionButton @click="onAddNewFragment">
24+
<UniconsPlus />
1825
</AppActionButton>
1926
</div>
2027
</div>
@@ -65,6 +72,10 @@ const onCopySnippet = () => {
6572
track('snippets/copy')
6673
}
6774
75+
const onClickMarkdownPreview = () => {
76+
snippetStore.isMarkdownPreview = !snippetStore.isMarkdownPreview
77+
}
78+
6879
emitter.on('focus:snippet-name', () => {
6980
inputRef.value?.select()
7081
})

src/renderer/components/snippets/SnippetsView.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,19 @@
88
<template v-if="snippetStore.selected">
99
<SnippetHeader />
1010
<TheEditor
11+
v-if="!snippetStore.isMarkdownPreview"
1112
v-model="snippet"
1213
v-model:lang="lang"
14+
:snippet-id="snippetStore.selectedId!"
15+
:fragment-index="snippetStore.fragment"
16+
:is-search-mode="snippetStore.searchQuery?.length > 0"
1317
:fragments="snippetStore.isFragmentsShow"
1418
/>
19+
<TheMarkdown
20+
v-else
21+
:value="snippetStore.currentContent!"
22+
/>
1523
</template>
16-
1724
<div
1825
v-else-if="isShowPlaceholder"
1926
class="placeholder"

src/renderer/store/snippets.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ export const useSnippetStore = defineStore('snippets', {
2525
selected: undefined,
2626
selectedMultiple: [],
2727
fragment: 0,
28+
searchQuery: undefined,
2829
isContextState: false,
29-
searchQuery: undefined
30+
isMarkdownPreview: false
3031
}),
3132

3233
getters: {

src/shared/types/renderer/store/snippets.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface State {
1010
selected?: Snippet
1111
selectedMultiple: Snippet[]
1212
fragment: number
13-
isContextState: boolean
1413
searchQuery?: string
14+
isContextState: boolean
15+
isMarkdownPreview: boolean
1516
}

0 commit comments

Comments
 (0)