@standardnotes/markdown-hybrid 1.7.6

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.
@@ -0,0 +1 @@
1
+ /*! highlight.js v9.16.2 | BSD3 License | git.io/hljslicense */
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <base target="_blank">
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
7
+ <script type="text/javascript" src="lib/component-relay.js"></script>
8
+ <script type="text/javascript" src="vendor/highlightjs/highlightjs.js"></script>
9
+ <link rel="stylesheet" media="all" href="vendor/easymd/easymde.css">
10
+ <script type="text/javascript" src="vendor/easymd/easymd.js"></script>
11
+ <link rel="stylesheet" media="all" href="stylekit.css">
12
+ <title><%= htmlWebpackPlugin.options.title %></title>
13
+ </head>
14
+
15
+ <body class="sn-component" id="sn-advanced-markdown-editor">
16
+ <textarea dir=auto id="editor" hidden></textarea>
17
+ </body>
18
+ </html>
@@ -0,0 +1,8 @@
1
+ {
2
+ "identifier": "org.standardnotes.advanced-markdown-editor-dev",
3
+ "name": "Markdown Pro - Development",
4
+ "content_type": "SN|Component",
5
+ "area": "editor-editor",
6
+ "version": "1.0.0",
7
+ "url": "http://localhost:8001/"
8
+ }
@@ -0,0 +1 @@
1
+ {}
Binary file
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@standardnotes/markdown-hybrid",
3
+ "version": "1.7.6",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "A Standard Notes derived editor that offers full support for Markdown editing.",
8
+ "main": "dist/dist.js",
9
+ "author": "Standard Notes.",
10
+ "license": "AGPL-3.0",
11
+ "sn": {
12
+ "main": "dist/index.html"
13
+ },
14
+ "scripts": {
15
+ "start": "webpack serve --config webpack.dev.js --progress --hot",
16
+ "build": "webpack --config webpack.prod.js",
17
+ "skip:lint": "eslint src --ext .js",
18
+ "skip:lint:fix": "eslint src --ext .js --fix",
19
+ "skip:test": "echo \"Error: no test specified\" && exit 0",
20
+ "skip:format": "prettier --write 'src/**/*.{html,css,scss,js,jsx,ts,tsx,json}' README.md"
21
+ },
22
+ "dependencies": {
23
+ "@standardnotes/component-relay": "standardnotes/component-relay#839ff5db9bc92db9d42cad8d202ddc4df729597d"
24
+ },
25
+ "devDependencies": {
26
+ "@babel/cli": "^7.13.10",
27
+ "@babel/core": "^7.13.13",
28
+ "@babel/preset-env": "^7.13.12",
29
+ "copy-webpack-plugin": "*",
30
+ "css-loader": "^5.2.0",
31
+ "dompurify": "^2.2.9",
32
+ "easymde": "2.16.1",
33
+ "eslint": "*",
34
+ "file-loader": "^6.2.0",
35
+ "font-awesome": "^4.7.0",
36
+ "highlightjs": "^9.16.2",
37
+ "html-webpack-plugin": "^5.3.1",
38
+ "marked": "^2.0.7",
39
+ "mini-css-extract-plugin": "^1.4.0",
40
+ "prettier": "*",
41
+ "sass": "^1.32.8",
42
+ "sass-loader": "^11.0.1",
43
+ "sn-stylekit": "3.0.1",
44
+ "webpack": "*",
45
+ "webpack-cli": "*",
46
+ "webpack-dev-server": "*",
47
+ "webpack-merge": "^5.8.0"
48
+ }
49
+ }
package/src/main.js ADDED
@@ -0,0 +1,371 @@
1
+ /* eslint-disable @typescript-eslint/no-var-requires */
2
+ document.addEventListener('DOMContentLoaded', function () {
3
+ let workingNote
4
+ let ignoreTextChange = false
5
+ let initialLoad = true
6
+ let lastValue, lastUUID, clientData
7
+ let renderNote = false
8
+ let showingUnsafeContentAlert = false
9
+
10
+ const componentRelay = new ComponentRelay({
11
+ targetWindow: window,
12
+ onReady: () => {
13
+ document.body.classList.add(componentRelay.platform)
14
+ document.body.classList.add(componentRelay.environment)
15
+
16
+ initializeEditor()
17
+ },
18
+ handleRequestForContentHeight: () => {
19
+ const baseHeight = 30
20
+ const toolbarHeight = document.querySelector('.editor-toolbar')?.offsetHeight
21
+ const scrollHeight = document.getElementsByClassName('CodeMirror-code')[0]?.scrollHeight
22
+ return baseHeight + toolbarHeight + scrollHeight
23
+ },
24
+ })
25
+
26
+ componentRelay.streamContextItem(async (note) => {
27
+ if (showingUnsafeContentAlert) {
28
+ return
29
+ }
30
+
31
+ if (note.uuid !== lastUUID) {
32
+ // Note changed, reset last values
33
+ lastValue = null
34
+ initialLoad = true
35
+ lastUUID = note.uuid
36
+ clientData = note.clientData
37
+ }
38
+
39
+ workingNote = note
40
+
41
+ // Only update UI on non-metadata updates.
42
+ if (note.isMetadataUpdate || !window.easymde) {
43
+ return
44
+ }
45
+
46
+ document
47
+ .getElementsByClassName('CodeMirror-code')[0]
48
+ .setAttribute('spellcheck', JSON.stringify(note.content.spellcheck))
49
+
50
+ const isUnsafeContent = checkIfUnsafeContent(note.content.text)
51
+ if (isUnsafeContent) {
52
+ const trustUnsafeContent = clientData['trustUnsafeContent'] ?? false
53
+ if (!trustUnsafeContent) {
54
+ const result = await showUnsafeContentAlert()
55
+ if (result) {
56
+ setTrustUnsafeContent(workingNote)
57
+ }
58
+ renderNote = result
59
+ } else {
60
+ renderNote = true
61
+ }
62
+ } else {
63
+ renderNote = true
64
+ }
65
+
66
+ /**
67
+ * If the user decides not to continue rendering the note,
68
+ * clear the editor and disable it.
69
+ */
70
+ if (!renderNote) {
71
+ window.easymde.value('')
72
+ if (!window.easymde.isPreviewActive()) {
73
+ window.easymde.togglePreview()
74
+ }
75
+ return
76
+ }
77
+
78
+ if (note.content.text !== lastValue) {
79
+ ignoreTextChange = true
80
+ window.easymde.value(note.content.text)
81
+ ignoreTextChange = false
82
+ }
83
+
84
+ if (initialLoad) {
85
+ initialLoad = false
86
+ window.easymde.codemirror.getDoc().clearHistory()
87
+ const mode = clientData && clientData.mode
88
+
89
+ // Set initial editor mode
90
+ if (mode === 'preview') {
91
+ if (!window.easymde.isPreviewActive()) {
92
+ window.easymde.togglePreview()
93
+ }
94
+ } else if (mode === 'split') {
95
+ if (!window.easymde.isSideBySideActive()) {
96
+ window.easymde.toggleSideBySide()
97
+ }
98
+ // falback config
99
+ } else if (window.easymde.isPreviewActive()) {
100
+ window.easymde.togglePreview()
101
+ }
102
+ }
103
+ })
104
+
105
+ function initializeEditor() {
106
+ window.easymde = new EasyMDE({
107
+ element: document.getElementById('editor'),
108
+ autoDownloadFontAwesome: false,
109
+ spellChecker: false,
110
+ nativeSpellcheck: true,
111
+ inputStyle: getInputStyleForEnvironment(),
112
+ status: false,
113
+ shortcuts: {
114
+ toggleSideBySide: 'Cmd-Alt-P',
115
+ },
116
+ // Syntax highlighting is disabled until we figure out performance issue: https://github.com/sn-extensions/advanced-markdown-editor/pull/20#issuecomment-513811633
117
+ // renderingConfig: {
118
+ // codeSyntaxHighlighting: true
119
+ // },
120
+ toolbar: [
121
+ {
122
+ className: 'fa fa-eye',
123
+ default: true,
124
+ name: 'preview',
125
+ noDisable: true,
126
+ title: 'Toggle Preview',
127
+ action: function () {
128
+ window.easymde.togglePreview()
129
+ saveMetadata()
130
+ },
131
+ },
132
+ {
133
+ className: 'fa fa-columns',
134
+ default: true,
135
+ name: 'side-by-side',
136
+ noDisable: true,
137
+ noMobile: true,
138
+ title: 'Toggle Side by Side',
139
+ action: function () {
140
+ window.easymde.toggleSideBySide()
141
+ saveMetadata()
142
+ },
143
+ },
144
+ '|',
145
+ 'heading',
146
+ 'bold',
147
+ 'italic',
148
+ 'strikethrough',
149
+ '|',
150
+ 'quote',
151
+ 'code',
152
+ '|',
153
+ 'unordered-list',
154
+ 'ordered-list',
155
+ '|',
156
+ 'clean-block',
157
+ '|',
158
+ 'link',
159
+ 'image',
160
+ '|',
161
+ 'table',
162
+ ],
163
+ })
164
+
165
+ /**
166
+ * Can be set to Infinity to make sure the whole document is always rendered,
167
+ * and thus the browser's text search works on it. This will have bad effects
168
+ * on performance of big documents.Really bad performance on Safari. Unusable.
169
+ */
170
+ window.easymde.codemirror.setOption('viewportMargin', 100)
171
+
172
+ window.easymde.codemirror.on('change', function () {
173
+ const strip = (html) => {
174
+ const tmp = document.implementation.createHTMLDocument('New').body
175
+ tmp.innerHTML = html
176
+ return tmp.textContent || tmp.innerText || ''
177
+ }
178
+
179
+ const truncateString = (string, limit = 90) => {
180
+ if (string.length <= limit) {
181
+ return string
182
+ } else {
183
+ return string.substring(0, limit) + '...'
184
+ }
185
+ }
186
+
187
+ if (!ignoreTextChange && renderNote) {
188
+ if (workingNote) {
189
+ // Be sure to capture this object as a variable, as this.note may be reassigned in `streamContextItem`, so by the time
190
+ // you modify it in the presave block, it may not be the same object anymore, so the presave values will not be applied to
191
+ // the right object, and it will save incorrectly.
192
+ const note = workingNote
193
+
194
+ componentRelay.saveItemWithPresave(note, () => {
195
+ lastValue = window.easymde.value()
196
+
197
+ let html = window.easymde.options.previewRender(window.easymde.value())
198
+ let strippedHtml = truncateString(strip(html))
199
+
200
+ note.content.preview_plain = strippedHtml
201
+ note.content.preview_html = null
202
+ note.content.text = lastValue
203
+ })
204
+ }
205
+ }
206
+ })
207
+
208
+ /**
209
+ * Scrolls the cursor into view, so the soft keyboard on mobile devices
210
+ * doesn't overlap the cursor. A short delay is added to prevent scrolling
211
+ * before the keyboard is shown.
212
+ */
213
+ const scrollCursorIntoView = (editor) => {
214
+ setTimeout(() => editor.scrollIntoView(), 200)
215
+ }
216
+
217
+ window.easymde.codemirror.on('cursorActivity', function (editor) {
218
+ if (componentRelay.environment !== 'mobile') {
219
+ return
220
+ }
221
+ scrollCursorIntoView(editor)
222
+ })
223
+
224
+ // Some sort of issue on Mobile RN where this causes an exception (".className is not defined")
225
+ try {
226
+ window.easymde.toggleFullScreen()
227
+ } catch (e) {
228
+ console.error('Error:', e)
229
+ }
230
+ }
231
+
232
+ function saveMetadata() {
233
+ if (!renderNote) {
234
+ return
235
+ }
236
+
237
+ const getEditorMode = () => {
238
+ const editor = window.easymde
239
+
240
+ if (editor) {
241
+ if (editor.isPreviewActive()) {
242
+ return 'preview'
243
+ }
244
+ if (editor.isSideBySideActive()) {
245
+ return 'split'
246
+ }
247
+ }
248
+ return 'edit'
249
+ }
250
+
251
+ const note = workingNote
252
+
253
+ componentRelay.saveItemWithPresave(note, () => {
254
+ note.clientData = {
255
+ ...note.clientData,
256
+ mode: getEditorMode(),
257
+ }
258
+ })
259
+ }
260
+
261
+ function setTrustUnsafeContent(note) {
262
+ componentRelay.saveItemWithPresave(note, () => {
263
+ note.clientData = {
264
+ ...note.clientData,
265
+ trustUnsafeContent: true,
266
+ }
267
+ })
268
+ }
269
+
270
+ /**
271
+ * Checks if a markdown text is safe to render.
272
+ */
273
+ function checkIfUnsafeContent(markdownText) {
274
+ const marked = require('marked')
275
+ const DOMPurify = require('dompurify')
276
+
277
+ /**
278
+ * Using marked to get the resulting HTML string from the markdown text.
279
+ */
280
+ const renderedHtml = marked(markdownText, {
281
+ headerIds: false,
282
+ smartypants: true,
283
+ })
284
+
285
+ const sanitizedHtml = DOMPurify.sanitize(renderedHtml, {
286
+ /**
287
+ * We don't need script or style tags.
288
+ */
289
+ FORBID_TAGS: ['script', 'style'],
290
+ /**
291
+ * XSS payloads can be injected via these attributes.
292
+ */
293
+ FORBID_ATTR: [
294
+ 'onerror',
295
+ 'onload',
296
+ 'onunload',
297
+ 'onclick',
298
+ 'ondblclick',
299
+ 'onmousedown',
300
+ 'onmouseup',
301
+ 'onmouseover',
302
+ 'onmousemove',
303
+ 'onmouseout',
304
+ 'onfocus',
305
+ 'onblur',
306
+ 'onkeypress',
307
+ 'onkeydown',
308
+ 'onkeyup',
309
+ 'onsubmit',
310
+ 'onreset',
311
+ 'onselect',
312
+ 'onchange',
313
+ ],
314
+ })
315
+
316
+ /**
317
+ * Create documents from both the sanitized string and the rendered string.
318
+ * This will allow us to compare them, and if they are not equal
319
+ * (i.e: do not contain the same properties, attributes, inner text, etc)
320
+ * it means something was stripped.
321
+ */
322
+ const renderedDom = new DOMParser().parseFromString(renderedHtml, 'text/html')
323
+ const sanitizedDom = new DOMParser().parseFromString(sanitizedHtml, 'text/html')
324
+ return !renderedDom.isEqualNode(sanitizedDom)
325
+ }
326
+
327
+ function showUnsafeContentAlert() {
328
+ if (showingUnsafeContentAlert) {
329
+ return
330
+ }
331
+
332
+ showingUnsafeContentAlert = true
333
+
334
+ const text =
335
+ 'We’ve detected that this note contains a script or code snippet which may be unsafe to execute. ' +
336
+ 'Scripts executed in the editor have the ability to impersonate as the editor to Standard Notes. ' +
337
+ 'Press Continue to mark this script as safe and proceed, or Cancel to avoid rendering this note.'
338
+
339
+ return new Promise((resolve) => {
340
+ const Stylekit = require('sn-stylekit')
341
+ const alert = new Stylekit.SKAlert({
342
+ title: null,
343
+ text,
344
+ buttons: [
345
+ {
346
+ text: 'Cancel',
347
+ style: 'neutral',
348
+ action: function () {
349
+ showingUnsafeContentAlert = false
350
+ resolve(false)
351
+ },
352
+ },
353
+ {
354
+ text: 'Continue',
355
+ style: 'danger',
356
+ action: function () {
357
+ showingUnsafeContentAlert = false
358
+ resolve(true)
359
+ },
360
+ },
361
+ ],
362
+ })
363
+ alert.present()
364
+ })
365
+ }
366
+
367
+ function getInputStyleForEnvironment() {
368
+ const environment = componentRelay.environment ?? 'web'
369
+ return environment === 'mobile' ? 'textarea' : 'contenteditable'
370
+ }
371
+ })