@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.
- package/.babelrc +5 -0
- package/.eslintignore +5 -0
- package/.eslintrc +20 -0
- package/CHANGELOG.md +100 -0
- package/LICENSE +661 -0
- package/README.md +44 -0
- package/dist/dist.css +2341 -0
- package/dist/dist.css.map +1 -0
- package/dist/dist.js +3 -0
- package/dist/dist.js.LICENSE.txt +1 -0
- package/dist/dist.js.map +1 -0
- package/dist/fonts/fontawesome-webfont.eot +0 -0
- package/dist/fonts/fontawesome-webfont.svg +2671 -0
- package/dist/fonts/fontawesome-webfont.ttf +0 -0
- package/dist/fonts/fontawesome-webfont.woff +0 -0
- package/dist/fonts/fontawesome-webfont.woff2 +0 -0
- package/dist/index.html +1 -0
- package/dist/lib/component-relay.js +1 -0
- package/dist/lib/component-relay.js.LICENSE.txt +1 -0
- package/dist/stylekit.css +3347 -0
- package/dist/vendor/easymd/easymd.js +2 -0
- package/dist/vendor/easymd/easymd.js.LICENSE.txt +6 -0
- package/dist/vendor/easymd/easymde.css +7 -0
- package/dist/vendor/highlightjs/highlightjs.js +2 -0
- package/dist/vendor/highlightjs/highlightjs.js.LICENSE.txt +1 -0
- package/editor.index.ejs +18 -0
- package/ext.json.sample +8 -0
- package/linter.tsconfig.json +1 -0
- package/markdown_pro_editor_bar.png +0 -0
- package/package.json +49 -0
- package/src/main.js +371 -0
- package/src/main.scss +339 -0
- package/webpack.config.js +73 -0
- package/webpack.dev.js +23 -0
- package/webpack.prod.js +7 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/*! highlight.js v9.16.2 | BSD3 License | git.io/hljslicense */
|
package/editor.index.ejs
ADDED
|
@@ -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>
|
package/ext.json.sample
ADDED
|
@@ -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
|
+
})
|