latex-stickies 1.4.2 → 1.4.4

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 CHANGED
@@ -169,6 +169,7 @@ node scripts/render-check.js # the first-run note renders correctly
169
169
  node scripts/ghost-check.js # autocomplete suggests, and Tab accepts
170
170
  node scripts/snapshot-check.js # a long note is captured whole
171
171
  node scripts/conflict-check.js # the changed-on-disk banner behaves
172
+ node scripts/denied-check.js # a notes folder it may not read is reported
172
173
  node scripts/verify-install.js # the install path, end to end (macOS)
173
174
  ```
174
175
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latex-stickies",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "Sticky notes for your desktop that render LaTeX and Markdown.",
5
5
  "main": "src/main.js",
6
6
  "scripts": {
@@ -25,6 +25,9 @@
25
25
  ],
26
26
  "mac": {
27
27
  "category": "public.app-category.productivity",
28
+ "extendInfo": {
29
+ "NSDocumentsFolderUsageDescription": "LaTeX Stickies keeps your notes as Markdown files in Documents, so you can read and edit them with any other app."
30
+ },
28
31
  "target": [
29
32
  "dmg",
30
33
  "zip"
@@ -94,6 +94,10 @@ function isCurrent(version) {
94
94
  }
95
95
  }
96
96
 
97
+ /** Shown in the macOS prompt asking to let the app read the notes folder. */
98
+ const DOCUMENTS_REASON = 'LaTeX Stickies keeps your notes as Markdown files in '
99
+ + 'Documents, so you can read and edit them with any other app.';
100
+
97
101
  function setPlist(plist, key, value) {
98
102
  try {
99
103
  run('plutil', ['-replace', key, '-string', value, plist]);
@@ -126,6 +130,10 @@ function build(app, version) {
126
130
  setPlist(plist, 'CFBundleDisplayName', NAME);
127
131
  setPlist(plist, 'CFBundleIdentifier', APP_ID);
128
132
  setPlist(plist, 'CFBundleExecutable', NAME);
133
+ // Without this key macOS denies the notes folder without ever asking. The
134
+ // app then starts, reads nothing and shows no window, which looks exactly
135
+ // like a launch that failed.
136
+ setPlist(plist, 'NSDocumentsFolderUsageDescription', DOCUMENTS_REASON);
129
137
 
130
138
  const from = path.join(TARGET, 'Contents', 'MacOS', 'Electron');
131
139
  const to = path.join(TARGET, 'Contents', 'MacOS', NAME);
@@ -20,7 +20,9 @@ import {
20
20
  defaultKeymap, history, historyKeymap, indentWithTab,
21
21
  cursorLineUp, cursorLineDown, selectLineUp, selectLineDown,
22
22
  } from '@codemirror/commands';
23
- import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
23
+ import {
24
+ markdown, markdownLanguage, insertNewlineContinueMarkup,
25
+ } from '@codemirror/lang-markdown';
24
26
  import {
25
27
  syntaxTree, HighlightStyle, syntaxHighlighting, defaultHighlightStyle,
26
28
  LanguageDescription,
@@ -70,7 +72,7 @@ window.CM = {
70
72
  EditorView, keymap, Decoration, WidgetType, ViewPlugin,
71
73
  defaultKeymap, history, historyKeymap, indentWithTab,
72
74
  cursorLineUp, cursorLineDown, selectLineUp, selectLineDown,
73
- markdown, markdownLanguage,
75
+ markdown, markdownLanguage, insertNewlineContinueMarkup,
74
76
  syntaxTree, HighlightStyle, syntaxHighlighting, defaultHighlightStyle,
75
77
  tags, codeLanguages,
76
78
  search, searchKeymap, highlightSelectionMatches, openSearchPanel,
package/src/main.js CHANGED
@@ -527,6 +527,37 @@ function watchNotesFolder() {
527
527
  });
528
528
  }
529
529
 
530
+ /**
531
+ * macOS can refuse the notes folder outright, and quietly.
532
+ *
533
+ * Documents is protected, and permission is tied to the app's signature -- so
534
+ * a rebuilt copy is a different app to the system and starts again with no
535
+ * access. Reading the folder then fails, nothing is restored and no window
536
+ * opens, which is indistinguishable from a launch that crashed. Say what
537
+ * happened instead, and offer the settings pane that fixes it.
538
+ */
539
+ function reportNoAccess(err) {
540
+ // Printed as well as shown: the dialog is the answer for the person at the
541
+ // screen, this line is what a harness or a bug report can see.
542
+ console.error(`notes folder refused: ${err.code} ${store.DIR}`);
543
+ const settings = 'x-apple.systempreferences:com.apple.preference.security'
544
+ + '?Privacy_DocumentsFolder';
545
+ const choice = dialog.showMessageBoxSync({
546
+ type: 'error',
547
+ message: 'LaTeX Stickies cannot read your notes',
548
+ detail: `macOS is not letting it open ${store.DIR}.\n\n`
549
+ + 'Allow it under Privacy & Security, in Files and Folders, then open '
550
+ + `the app again.\n\n(${err.code}: ${err.message})`,
551
+ buttons: ['Open Privacy Settings', 'Quit'],
552
+ defaultId: 0,
553
+ });
554
+ if (choice === 0 && process.platform === 'darwin') shell.openExternal(settings);
555
+ app.quit();
556
+ }
557
+
558
+ /** True when the filesystem said no, rather than the folder being absent. */
559
+ const denied = (err) => err && (err.code === 'EPERM' || err.code === 'EACCES');
560
+
530
561
  app.whenReady().then(async () => {
531
562
  // Run from npm there is no .app bundle of our own to carry the icon, so
532
563
  // macOS would show the generic Electron atom in the Dock. Set it explicitly.
@@ -546,9 +577,15 @@ app.whenReady().then(async () => {
546
577
  }
547
578
  }
548
579
 
549
- await buildMenu();
550
- restoreNotes();
551
- watchNotesFolder();
580
+ try {
581
+ await buildMenu();
582
+ restoreNotes();
583
+ watchNotesFolder();
584
+ } catch (err) {
585
+ if (!denied(err)) throw err;
586
+ reportNoAccess(err);
587
+ return;
588
+ }
552
589
 
553
590
  if (SMOKE_CLOSE_MS) {
554
591
  setTimeout(() => {
@@ -19,7 +19,7 @@ const {
19
19
  WidgetType, ViewPlugin, keymap, Prec,
20
20
  defaultKeymap, history, historyKeymap, indentWithTab,
21
21
  cursorLineUp, cursorLineDown, selectLineUp, selectLineDown,
22
- markdown, markdownLanguage, codeLanguages,
22
+ markdown, markdownLanguage, codeLanguages, insertNewlineContinueMarkup,
23
23
  syntaxTree, HighlightStyle, syntaxHighlighting, defaultHighlightStyle, tags,
24
24
  search, searchKeymap, highlightSelectionMatches,
25
25
  } = window.CM;
@@ -312,6 +312,8 @@ const HIDE = Decoration.replace({});
312
312
  */
313
313
  const MARKER = Decoration.mark({ class: 'cm-md-marker' });
314
314
 
315
+
316
+
315
317
  /** Elements whose markers are shown while the caret is inside them. */
316
318
  const REVEAL = new Set([
317
319
  'Emphasis', 'StrongEmphasis', 'Strikethrough', 'InlineCode', 'FencedCode',
@@ -445,6 +447,23 @@ function buildDecorations(state) {
445
447
  const first = state.doc.lineAt(node.from).number;
446
448
  const last = state.doc.lineAt(node.to).number;
447
449
  for (let n = first; n <= last; n++) addLine(state.doc.line(n).from, cls);
450
+
451
+ // A quote inside a quote is a quote inside a quote, and drawing every
452
+ // level the same made ">" and ">>" identical on screen. The outer
453
+ // node is entered first, so the deeper class lands only on the lines
454
+ // that are actually deeper. Capped because past three the indent eats
455
+ // the width of a sticky note.
456
+ if (node.name === 'Blockquote') {
457
+ let depth = 0;
458
+ for (let up = node.node; up; up = up.parent) {
459
+ if (up.name === 'Blockquote') depth += 1;
460
+ }
461
+ if (depth > 1) {
462
+ const level = `cm-md-quote-${Math.min(depth, 3)}`;
463
+ for (let n = first; n <= last; n++) addLine(state.doc.line(n).from, level);
464
+ }
465
+ }
466
+
448
467
  if (node.name === 'FencedCode') {
449
468
  addLine(state.doc.line(first).from, 'cm-md-code-first');
450
469
  addLine(state.doc.line(last).from, 'cm-md-code-last');
@@ -616,8 +635,13 @@ const markerReveal = ViewPlugin.fromClass(class {
616
635
  this.sync();
617
636
  }
618
637
 
619
- update(update) {
620
- if (update.docChanged || update.selectionSet || update.viewportChanged) this.sync();
638
+ update() {
639
+ // Every update, not only the ones that moved the caret. The parser runs
640
+ // behind the keystroke, so the first "#" of a heading arrives as a plain
641
+ // character and becomes a marker on a later update that changed neither
642
+ // the document nor the selection. Skipping those left the reveal one
643
+ // keystroke behind: a heading stayed hidden while it was being typed.
644
+ this.sync();
621
645
  }
622
646
 
623
647
  destroy() {
@@ -630,12 +654,21 @@ const markerReveal = ViewPlugin.fromClass(class {
630
654
  let from = -1;
631
655
  let to = -1;
632
656
  if (!snapshotMode) {
633
- for (let node = syntaxTree(view.state).resolveInner(sel.from, 1); node; node = node.parent) {
634
- if (REVEAL.has(node.name) && sel.to <= node.to) {
635
- from = node.from;
636
- to = node.to;
637
- break;
657
+ const tree = syntaxTree(view.state);
658
+ // Both sides of the caret, and the side before it first. Typing "###"
659
+ // leaves the caret at the end of the line, where the node *starting*
660
+ // here is the document, not the heading -- so looking forward only, a
661
+ // heading stayed hidden the whole time you were typing it.
662
+ const enclosing = (side) => {
663
+ for (let node = tree.resolveInner(sel.from, side); node; node = node.parent) {
664
+ if (REVEAL.has(node.name) && sel.to <= node.to) return node;
638
665
  }
666
+ return null;
667
+ };
668
+ const node = enclosing(-1) || enclosing(1);
669
+ if (node) {
670
+ from = node.from;
671
+ to = node.to;
639
672
  }
640
673
  }
641
674
  for (const el of view.contentDOM.querySelectorAll('.cm-md-marker')) {
@@ -669,9 +702,24 @@ const livePreview = StateField.define({
669
702
 
670
703
  /* ---------- how markdown reads ---------- */
671
704
 
705
+ /**
706
+ * Markers are punctuation, not prose, and should not compete with the text
707
+ * they mark up. CodeMirror's default paints them #404740 -- all but the ink
708
+ * colour -- because every marker carries tags.processingInstruction, a child
709
+ * of meta, and defaultHighlightStyle is loaded here for its code colours.
710
+ *
711
+ * A flat grey rather than a faded ink: fading a marker into the paper is what
712
+ * made an H1 and an H2 indistinguishable while one was being typed.
713
+ */
714
+ const MARKER_INK = '#757575';
715
+
672
716
  const markdownStyle = HighlightStyle.define([
673
717
  // textDecoration: 'none' is deliberate -- defaultHighlightStyle, loaded for
674
718
  // the colours inside code fences, underlines every heading.
719
+ // One entry covers every marker: the hashes, the stars, the backticks, the
720
+ // quote arrow, the list dash, a table's pipes and a strikethrough's tildes
721
+ // all carry this tag.
722
+ { tag: tags.processingInstruction, color: MARKER_INK },
675
723
  { tag: tags.heading, textDecoration: 'none' },
676
724
  { tag: tags.heading1, fontSize: '1.35em', fontWeight: '600', textDecoration: 'none' },
677
725
  { tag: tags.heading2, fontSize: '1.18em', fontWeight: '600', textDecoration: 'none' },
@@ -777,6 +825,17 @@ function verticalStep(forward, base, extend) {
777
825
  };
778
826
  }
779
827
 
828
+ /**
829
+ * Enter carries a list on: a new bullet, the next number, another empty task
830
+ * box, or a second quote line -- and a second Enter on an item with nothing in
831
+ * it ends the list instead of adding to it.
832
+ *
833
+ * Without this, pressing Enter after "- [ ] milk" left a bare line, and typing
834
+ * "[ ] eggs" there looks like a task and is not one: the box only means
835
+ * anything inside a list item.
836
+ */
837
+ const continueList = [{ key: 'Enter', run: insertNewlineContinueMarkup }];
838
+
780
839
  const verticalKeymap = [
781
840
  { key: 'ArrowUp', run: verticalStep(false, cursorLineUp), preventDefault: true },
782
841
  { key: 'ArrowDown', run: verticalStep(true, cursorLineDown), preventDefault: true },
@@ -804,6 +863,7 @@ function createLiveEditor({ parent, doc, onChange }) {
804
863
  // Ahead of the defaults, so Mod-i and friends are not swallowed.
805
864
  Prec.high(keymap.of(shortcuts)),
806
865
  Prec.high(keymap.of(verticalKeymap)),
866
+ Prec.high(keymap.of(continueList)),
807
867
  // Search ahead of the defaults: Cmd+F must open the panel rather than
808
868
  // fall through to anything else bound to it.
809
869
  Prec.high(keymap.of(searchKeymap)),
@@ -154,11 +154,36 @@ body.editing #tools > * { visibility: visible; }
154
154
  .cm-md-h3 { font-size: 1.05em; font-weight: 600; }
155
155
  .cm-md-h4, .cm-md-h5, .cm-md-h6 { font-weight: 600; }
156
156
 
157
- .cm-md-quote {
158
- border-left: 2px solid rgba(0, 0, 0, 0.22);
159
- padding-left: 0.7em;
157
+ /* Scoped to .cm-editor because `.cm-editor .cm-line { padding: 0 }` above
158
+ zeroes it, and a bare .cm-md-quote loses to that -- which is why quoted
159
+ text sat hard against its own bar with no inset at all. The padding also
160
+ gives a wrapped quote its hanging indent, for nothing. */
161
+ .cm-editor .cm-md-quote {
162
+ --quote-depth: 1;
163
+ position: relative;
164
+ padding-left: calc(var(--quote-depth) * 0.9em + 0.4em);
160
165
  opacity: 0.85;
161
166
  }
167
+ .cm-editor .cm-md-quote-2 { --quote-depth: 2; }
168
+ .cm-editor .cm-md-quote-3 { --quote-depth: 3; }
169
+
170
+ /* One bar per nesting level, drawn as a repeating gradient rather than a
171
+ border: a line has only one border-left however deep the quote is. The
172
+ gradient repeats on the same interval as the indent, so it draws exactly
173
+ as many bars as there are levels. */
174
+ .cm-editor .cm-md-quote::before {
175
+ content: '';
176
+ position: absolute;
177
+ left: 0;
178
+ top: 0;
179
+ bottom: 0;
180
+ width: calc(var(--quote-depth) * 0.9em);
181
+ background: repeating-linear-gradient(
182
+ to right,
183
+ rgba(0, 0, 0, 0.22) 0 2px,
184
+ transparent 2px 0.9em
185
+ );
186
+ }
162
187
 
163
188
  /* Smaller text, but the same line box as prose. CodeMirror moves the caret
164
189
  vertically by one default line height, so a line shorter than that gets
@@ -237,9 +262,13 @@ body.editing #tools > * { visibility: visible; }
237
262
  opacity: 0;
238
263
  transition: max-width 0.12s ease, opacity 0.12s ease;
239
264
  }
265
+ /* Full strength, not a hint. A revealed marker is there to be edited -- you
266
+ are counting hashes to see what level a heading is, or putting the caret
267
+ between a pair of stars -- and a washed-out one is hard to read against
268
+ the paper. It is the hiding that keeps the note clean, not the fading. */
240
269
  .cm-editor .cm-md-marker-open {
241
270
  max-width: 4em;
242
- opacity: 0.35;
271
+ opacity: 1;
243
272
  }
244
273
 
245
274
  /* Sits at the end of a block's opening fence line, beside the language name. */