@standardnotes/markdown-basic 1.6.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/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # Markdown Basic
2
+
3
+ <div align="center">
4
+
5
+ [![License](https://img.shields.io/github/license/sn-extensions/markdown-basic?color=blue)](https://github.com/sn-extensions/markdown-basic/blob/master/LICENSE)
6
+ [![GitHub issues and feature requests](https://img.shields.io/github/issues/sn-extensions/markdown-basic.svg)](https://github.com/sn-extensions/markdown-basic/issues/)
7
+ [![Slack](https://img.shields.io/badge/slack-standardnotes-CC2B5E.svg?style=flat&logo=slack)](https://standardnotes.org/slack)
8
+ [![GitHub Stars](https://img.shields.io/github/stars/sn-extensions/markdown-basic?style=social)](https://github.com/sn-extensions/markdown-basic)
9
+
10
+ </div>
11
+
12
+ ## Introduction
13
+
14
+ Markdown Basic is a [custom editor](https://standardnotes.org/help/77/what-are-editors) for [Standard Notes](https://standardnotes.org), a free, open-source, and [end-to-end encrypted](https://standardnotes.org/knowledge/2/what-is-end-to-end-encryption) notes app.
15
+
16
+ ## Features
17
+
18
+ - Markdown via Markdown-It
19
+ - Syntax Highlighting via Highlight.js
20
+ - Optional split pane view
21
+ - Task Lists
22
+ - Tables
23
+ - Footnotes
24
+ - Inline external images
25
+
26
+ ## Installation
27
+
28
+ 1. Register for an account at Standard Notes using the [Desktop App](https://standardnotes.org/download) or [Web app](https://app.standardnotes.org). Remember to use a strong and memorable password.
29
+ 2. Sign up for [Standard Notes Extended](https://dashboard.standardnotes.org/member). Then, follow the instructions [here](https://standardnotes.org/help/29/how-do-i-install-extensions-once-i-ve-signed-up-for-extended) or continue.
30
+ 3. Click **Extensions** in the lower left corner.
31
+ 4. Under **Repository**, find **Markdown Basic**.
32
+ 5. Click **Install**.
33
+ 6. Close the **Extensions** pop-up.
34
+ 7. At the top of your note, click **Editor**, then click **Markdown Basic**.
35
+ 8. Click **Continue**, and you are done!
36
+
37
+ After you have installed the editor on the web or desktop app, it will automatically sync to your [mobile app](https://standardnotes.org/download) after you log in.
38
+
39
+ ## Style Guide
40
+
41
+ | Result | Markdown |
42
+ | :----------------- | :------------------------------------------- |
43
+ | **Bold** | \*\*text\*\* or \_\_text\_\_ |
44
+ | _Emphasize_ | \*text\* or \_text\_ |
45
+ | ~~Strike-through~~ | \~\~text\~\~ |
46
+ | Link | [text]\(http://) |
47
+ | Image | ![text]\(http://) |
48
+ | `Inline Code` | \`code\` |
49
+ | Code Block | \`\`\`language <br></br>code <br></br>\`\`\` |
50
+ | Unordered List | \* item <br></br> - item <br></br> + item |
51
+ | Ordered List | 1. item |
52
+ | Task List | `- [ ] Task` or `- [x] Task` |
53
+ | Blockquote | \> quote |
54
+ | H1 | # Heading |
55
+ | H2 | ## Heading |
56
+ | H3 | ### Heading |
57
+ | H4 | #### Heading |
58
+ | Section Breaks | `---` or `***` |
59
+
60
+ ## Tables
61
+
62
+ Colons can be used to align columns.
63
+ Copy this into your editor to see what it renders:
64
+
65
+ ```md
66
+ | Tables | Are | Cool |
67
+ | ------------------ | :-----------: | ------: |
68
+ | col 2 is | centered | \$149 |
69
+ | col 3 is | right-aligned | \$4.17 |
70
+ | privacy is | neat | \$2.48 |
71
+ | rows don't need to | be pretty | what? |
72
+ | the last line is | unnecessary | really? |
73
+ | one more | row | Yay! 😆 |
74
+ ```
75
+
76
+ ## Footnotes
77
+
78
+ The Markdown Basic editor supports footnotes. The footnote links do not work properly on mobile. Copy this into your note to see how they're used:
79
+
80
+ ```md
81
+ You can create footnote references that are short[^1] or long.[^2]
82
+ You can also create them inline.^[which may be easier,
83
+ since you don't need to pick an identifier and move down to type the note]
84
+ The footnotes are automatically numbered at the bottom of your note,
85
+ but you'll need to manually number your superscripts.
86
+ Make sure to count your variable[^variable] footnotes.[^5]
87
+
88
+ [^1]: Here's a footnote.
89
+ [^2]: Here’s a footnote with multiple blocks.
90
+
91
+ Subsequent paragraphs are indented to show that they belong to the previous footnote.
92
+
93
+ { eight spaces for some code }
94
+
95
+ The whole paragraph can be indented, or just the first
96
+ line. In this way, multi-paragraph footnotes work like
97
+ multi-paragraph list items.
98
+
99
+ This paragraph won’t be part of the footnote, because it
100
+ isn’t indented.
101
+
102
+ [^variable]: The variable footnote is the fourth footnote.
103
+ [^5]: This is the fifth footnote.
104
+ ```
105
+
106
+ #### Not yet available:
107
+
108
+ - KaTeX
109
+ - Printing
110
+ - Custom Font Families
111
+ - Custom Font Sizes
112
+ - Superscript
113
+ - Subscript
114
+
115
+ ## License
116
+
117
+ [GNU Affero General Public License v3.0](https://github.com/sn-extensions/markdown-basic/blob/master/LICENSE)
118
+
119
+ ## Development
120
+
121
+ The instructions for local setup can be found [here](https://docs.standardnotes.org/extensions/local-setup). All commands are performed in the root directory:
122
+
123
+ 1. Fork the [repository](https://github.com/sn-extensions/markdown-basic) on GitHub
124
+ 2. [Clone](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) your fork of the repository
125
+ 3. Type `cd markdown-basic`
126
+ 4. Run `yarn` to locally install the packages in `package.json`
127
+ 5. Create `ext.json` as shown [here](https://docs.standardnotes.org/extensions/local-setup) with `url: "http://localhost:8004/dist/index.html"`. Optionally, create your `ext.json` as a copy of `ext.json.sample`.
128
+ 6. Install `http-server` using `yarn global add http-server` or `npm install -g http-server`
129
+ 7. Start the server at `http://localhost:8004` using `http-server . --cors -p 8004`
130
+ 8. Import the extension into the [web](https://app.standardnotes.org) or [desktop](https://standardnotes.org/download) app with `http://localhost:8004/ext.json`.
131
+ 9. To build the editor, open another command window and run `yarn build` or `npm run build`. For live builds, use `yarn watch` or `npm run watch`. You can also run `yarn start` or `npm run start` and open the editor at `http://localhost:8080`.
132
+
133
+ ## Further Resources
134
+
135
+ - [GitHub](https://github.com/sn-extensions/markdown-basic/)
136
+ - [Issues and Feature Requests](https://github.com/sn-extensions/markdown-basic/issues)
137
+ - [Standard Notes Slack](https://standardnotes.org/slack) (for connecting with the Standard Notes Community)
138
+ - [Standard Notes Help Files](https://standardnotes.org/help) (for issues related to Standard Notes but unrelated to this editor)
@@ -0,0 +1,312 @@
1
+ import React from 'react';
2
+ import ComponentRelay from '@standardnotes/component-relay';
3
+ const MarkdownIt = require('markdown-it');
4
+
5
+ const EditMode = 0;
6
+ const SplitMode = 1;
7
+ const PreviewMode = 2;
8
+
9
+ export default class Home extends React.Component {
10
+
11
+ constructor(props) {
12
+ super(props);
13
+
14
+ this.modes = [
15
+ { mode: EditMode, label: 'Edit', css: 'edit' },
16
+ { mode: SplitMode, label: 'Split', css: 'split' },
17
+ { mode: PreviewMode, label: 'Preview', css: 'preview' },
18
+ ];
19
+
20
+ this.state = { mode: this.modes[0] };
21
+ }
22
+
23
+ componentDidMount() {
24
+ this.simpleMarkdown = document.getElementById('simple-markdown');
25
+ this.editor = document.getElementById('editor');
26
+ this.preview = document.getElementById('preview');
27
+
28
+ this.configureMarkdown();
29
+ this.connectToBridge();
30
+ this.updatePreviewText();
31
+ this.addChangeListener();
32
+
33
+ this.configureResizer();
34
+ this.addTabHandler();
35
+
36
+ this.scrollTriggers = {};
37
+ this.scrollHandlers = [
38
+ { el: this.editor, handler: this.scrollHandler(this.editor, this.preview) },
39
+ { el: this.preview, handler: this.scrollHandler(this.preview, this.editor) }
40
+ ];
41
+ }
42
+
43
+ UNSAFE_componentWillUpdate(nextProps, nextState) {
44
+ let prevMode = this.state.mode.mode;
45
+ let nextMode = nextState.mode.mode;
46
+
47
+ // If we changed to Split mode we add the scroll listeners
48
+ if (prevMode !== nextMode) {
49
+ if (nextMode === SplitMode) {
50
+ this.addScrollListeners();
51
+ } else {
52
+ this.removeScrollListeners();
53
+ }
54
+ }
55
+ }
56
+
57
+ setModeFromModeValue(value) {
58
+ for (let mode of this.modes) {
59
+ if (mode.mode == value) {
60
+ this.setState({ mode });
61
+ return;
62
+ }
63
+ }
64
+ }
65
+
66
+ changeMode(mode) {
67
+ this.setState({ mode });
68
+ if (!this.note) {
69
+ return;
70
+ }
71
+ this.note.clientData = { mode: mode.mode };
72
+ this.componentRelay.saveItem(this.note);
73
+ }
74
+
75
+ configureMarkdown() {
76
+ const markdownitOptions = {
77
+ // automatically render raw links as anchors.
78
+ linkify: true,
79
+ // Convert '\n' in paragraphs into <br>
80
+ breaks: true
81
+ };
82
+
83
+ this.markdown = MarkdownIt(markdownitOptions)
84
+ .use(require('markdown-it-footnote'))
85
+ .use(require('markdown-it-task-lists'))
86
+ .use(require('markdown-it-highlightjs'));
87
+
88
+ // Remember old renderer, if overriden, or proxy to default renderer
89
+ const defaultRender = this.markdown.renderer.rules.link_open || ((tokens, idx, options, env, self) => {
90
+ return self.renderToken(tokens, idx, options);
91
+ });
92
+
93
+ this.markdown.renderer.rules.link_open = ((tokens, idx, options, env, self) => {
94
+ // If you are sure other plugins can't add `target` - drop check below
95
+ const aIndex = tokens[idx].attrIndex('target');
96
+
97
+ if (aIndex < 0) {
98
+ tokens[idx].attrPush(['target', '_blank']); // add new attribute
99
+ } else {
100
+ tokens[idx].attrs[aIndex][1] = '_blank'; // replace value of existing attr
101
+ }
102
+
103
+ // pass token to default renderer.
104
+ return defaultRender(tokens, idx, options, env, self);
105
+ });
106
+ }
107
+
108
+ connectToBridge() {
109
+ this.componentRelay = new ComponentRelay({
110
+ targetWindow: window,
111
+ onReady: () => {
112
+ const { platform } = this.componentRelay;
113
+ this.setState({ platform });
114
+ },
115
+ handleRequestForContentHeight: () => {
116
+ return undefined
117
+ },
118
+ });
119
+
120
+ this.componentRelay.streamContextItem((note) => {
121
+ this.note = note;
122
+
123
+ if (note.clientData) {
124
+ const mode = note.clientData.mode ?? EditMode;
125
+ this.setModeFromModeValue(mode);
126
+ }
127
+
128
+ // Only update UI on non-metadata updates.
129
+ if (note.isMetadataUpdate) {
130
+ return;
131
+ }
132
+
133
+ this.editor.value = note.content.text;
134
+ this.preview.innerHTML = this.markdown.render(note.content.text);
135
+
136
+ document.getElementById('editor').setAttribute(
137
+ 'spellcheck',
138
+ JSON.stringify(note.content.spellcheck)
139
+ );
140
+ });
141
+ }
142
+
143
+ truncateString(string, limit = 80) {
144
+ if (!string) {
145
+ return null;
146
+ }
147
+ if (string.length <= limit) {
148
+ return string;
149
+ } else {
150
+ return string.substring(0, limit) + '...';
151
+ }
152
+ }
153
+
154
+ updatePreviewText() {
155
+ const text = this.editor.value || '';
156
+ this.preview.innerHTML = this.markdown.render(text);
157
+ return text;
158
+ }
159
+
160
+ addChangeListener() {
161
+ document.getElementById('editor').addEventListener('input', () => {
162
+ if (this.note) {
163
+ // Be sure to capture this object as a variable, as this.note may be reassigned in `streamContextItem`, so by the time
164
+ // you modify it in the presave block, it may not be the same object anymore, so the presave values will not be applied to
165
+ // the right object, and it will save incorrectly.
166
+ let note = this.note;
167
+
168
+ this.componentRelay.saveItemWithPresave(note, () => {
169
+ note.content.text = this.updatePreviewText();
170
+ note.content.preview_plain = this.truncateString(this.preview.textContent || this.preview.innerText);
171
+ note.content.preview_html = null;
172
+ });
173
+ }
174
+ });
175
+ }
176
+
177
+ addScrollListeners() {
178
+ this.scrollHandlers.forEach(({ el, handler }) => el.addEventListener('scroll', handler));
179
+ }
180
+
181
+ removeScrollListeners() {
182
+ this.scrollHandlers.forEach(({ el, handler }) => el.removeEventListener('scroll', handler));
183
+ }
184
+
185
+ scrollHandler = (source, destination) => {
186
+ let frameRequested;
187
+
188
+ return (event) => {
189
+ // Avoid the cascading effect by not handling the event if it was triggered initially by this element
190
+ if (this.scrollTriggers[source] === true) {
191
+ this.scrollTriggers[source] = false;
192
+ return;
193
+ }
194
+ this.scrollTriggers[source] = true;
195
+
196
+ // Only request the animation frame once until it gets processed
197
+ if (frameRequested) {
198
+ return;
199
+ }
200
+ frameRequested = true;
201
+
202
+ window.requestAnimationFrame(() => {
203
+ let target = event.target;
204
+ let height = target.scrollHeight - target.clientHeight;
205
+ let ratio = parseFloat(target.scrollTop) / height;
206
+ let move = (destination.scrollHeight - destination.clientHeight) * ratio;
207
+ destination.scrollTop = move;
208
+
209
+ frameRequested = false;
210
+ });
211
+ };
212
+ }
213
+
214
+ removeSelection() {
215
+ if (window.getSelection) {
216
+ window.getSelection().removeAllRanges();
217
+ } else if (document.selection) {
218
+ document.selection.empty();
219
+ }
220
+ }
221
+
222
+ configureResizer() {
223
+ let pressed = false;
224
+ const columnResizer = document.getElementById('column-resizer');
225
+ const resizerWidth = columnResizer.offsetWidth;
226
+ const safetyOffset = 15;
227
+
228
+ columnResizer.addEventListener('mousedown', () => {
229
+ pressed = true;
230
+ columnResizer.classList.add('dragging');
231
+ this.editor.classList.add('no-selection');
232
+ });
233
+
234
+ document.addEventListener('mousemove', (event) => {
235
+ if (!pressed) {
236
+ return;
237
+ }
238
+
239
+ let x = event.clientX;
240
+ if (x < resizerWidth / 2 + safetyOffset) {
241
+ x = resizerWidth / 2 + safetyOffset;
242
+ } else if (x > this.simpleMarkdown.offsetWidth - resizerWidth - safetyOffset) {
243
+ x = this.simpleMarkdown.offsetWidth - resizerWidth - safetyOffset;
244
+ }
245
+
246
+ const colLeft = x - resizerWidth / 2;
247
+ columnResizer.style.left = colLeft + 'px';
248
+ this.editor.style.width = (colLeft - safetyOffset) + 'px';
249
+
250
+ this.removeSelection();
251
+ });
252
+
253
+ document.addEventListener('mouseup', () => {
254
+ if (pressed) {
255
+ pressed = false;
256
+ columnResizer.classList.remove('dragging');
257
+ this.editor.classList.remove('no-selection');
258
+ }
259
+ });
260
+ }
261
+
262
+ addTabHandler() {
263
+ // Tab handler
264
+ this.editor.addEventListener('keydown', (event) => {
265
+ if (!event.shiftKey && event.which == 9) {
266
+ event.preventDefault();
267
+
268
+ // Using document.execCommand gives us undo support
269
+ if (!document.execCommand('insertText', false, '\t')) {
270
+ // document.execCommand works great on Chrome/Safari but not Firefox
271
+ const start = this.selectionStart;
272
+ const end = this.selectionEnd;
273
+ const spaces = ' ';
274
+
275
+ // Insert 4 spaces
276
+ this.value = this.value.substring(0, start)
277
+ + spaces + this.value.substring(end);
278
+
279
+ // Place cursor 4 spaces away from where
280
+ // the tab key was pressed
281
+ this.selectionStart = this.selectionEnd = start + 4;
282
+ }
283
+ }
284
+ });
285
+ }
286
+
287
+ render() {
288
+ return (
289
+ <div id="simple-markdown" className={`sn-component ${this.state.platform}`}>
290
+ <div id="header">
291
+ <div className="segmented-buttons-container sk-segmented-buttons">
292
+ <div className="buttons">
293
+ {this.modes.map(mode =>
294
+ <div key={mode} onClick={() => this.changeMode(mode)} className={`sk-button button ${this.state.mode == mode ? 'selected info' : 'sk-secondary-contrast'}`}>
295
+ <div className="sk-label">
296
+ {mode.label}
297
+ </div>
298
+ </div>
299
+ )}
300
+ </div>
301
+ </div>
302
+ </div>
303
+
304
+ <div id="editor-container" className={this.state.mode.css}>
305
+ <textarea dir="auto" id="editor" className={this.state.mode.css}></textarea>
306
+ <div id="column-resizer" className={this.state.mode.css}></div>
307
+ <div id="preview" className={this.state.mode.css}></div>
308
+ </div>
309
+ </div>
310
+ );
311
+ }
312
+ }
package/app/index.html ADDED
@@ -0,0 +1,7 @@
1
+ <html>
2
+ <link rel="stylesheet" type="text/css" href="dist.css">
3
+ <title>Simple Markdown Editor</title>
4
+ <body>
5
+ <script type="text/javascript" src="dist.js"></script>
6
+ </body>
7
+ </html>
package/app/main.js ADDED
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import Home from './components/Home';
4
+
5
+ export default class App extends React.Component {
6
+ constructor(props) {
7
+ super(props);
8
+ }
9
+
10
+ render() {
11
+ return (
12
+ <div>
13
+ <Home />
14
+ </div>
15
+ );
16
+ }
17
+ }
18
+
19
+
20
+ ReactDOM.render(
21
+ <App />,
22
+ document.body.appendChild(document.createElement('div'))
23
+ );