1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-14 18:26:26 +00:00

Compare commits

...

184 Commits

Author SHA1 Message Date
dependabot[bot]
1bd0c451a4 Bump path-parse from 1.0.5 to 1.0.7
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.5 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-10 23:17:04 +00:00
Baptiste Augrain
58c4a78be1 avoids conflicting styles between inline codes and code blocks 2020-12-27 11:16:55 +09:00
Gonçalo Santos
2603dfc1ed Fix Analytics save bug 2020-12-12 00:15:37 +09:00
Gonçalo Santos
2df590600b AutoUpdate is auto saved 2020-12-12 00:15:37 +09:00
Gonçalo Santos
ef20a8f3e5 Remove debug statements 2020-12-12 00:15:37 +09:00
Gonçalo Santos
3e405e1abf Fix Cancel update 2020-12-12 00:15:37 +09:00
Gonçalo Santos
553832bdfa Create confirm download dialog 2020-12-12 00:15:37 +09:00
Gonçalo Santos
18d65d999a Menu item calls update-check 2020-12-12 00:15:37 +09:00
Gonçalo Santos
3b5eff582a Update not found message 2020-12-12 00:15:37 +09:00
Gonçalo Santos
85d09b3b3d Add update menu item 2020-12-12 00:15:37 +09:00
Baptiste Augrain
8958e67fcf fix unwanted deletion of attachments 2020-09-15 12:33:12 +09:00
Junyoung Choi
47b796909a v0.16.1 2020-09-04 23:47:01 +09:00
Junyoung Choi
67d76abdfa Merge branch 'master' of github.com:BoostIO/Boostnote 2020-09-04 23:45:41 +09:00
Junyoung Choi
d75d68ba72 Add email subscription form 2020-09-04 23:44:16 +09:00
Junyoung Choi
323be6b72d Merge pull request #2612 from daiyam/export-yfm
improve export
2020-07-22 18:24:27 +09:00
Junyoung Choi
031a113338 Merge branch 'master' of github.com:BoostIO/Boostnote 2020-07-20 21:07:26 +09:00
Junyoung Choi
b50c5386a6 Add BoostHub link to menu 2020-07-20 21:06:55 +09:00
Baptiste Augrain
65777b1d56 Merge branch 'master' into export-yfm 2020-07-20 14:05:21 +02:00
Junyoung Choi
fe728874ac Fix code sign script 2020-07-20 21:02:29 +09:00
Junyoung Choi
c5b4c327fa v0.16.0 2020-07-20 20:42:18 +09:00
Junyoung Choi
4c39922ead Merge pull request #2405 from daiyam/fix-scroll
Better scroll sync between the editor and the preview in the SplitEditor
2020-07-20 19:48:29 +09:00
Junyoung Choi
1cdc74a2f0 Merge pull request #2594 from daiyam/fix-autocomplete-codeblock
improve autocomplete within code blocks
2020-07-20 19:47:48 +09:00
Junyoung Choi
6213a820e6 Merge pull request #3600 from ZeroX-DG/date-iso8601
Add Date shortcut ISO 8601 format as an option in preference
2020-07-20 19:46:53 +09:00
Junyoung Choi
ce81b26d1d Merge branch 'master' into date-iso8601 2020-07-20 19:42:08 +09:00
dependabot[bot]
fae91255f9 Bump lodash from 4.17.13 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.13 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.13...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-20 19:28:17 +09:00
dependabot[bot]
a82a3efb14 Bump websocket-extensions from 0.1.3 to 0.1.4
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-20 19:27:17 +09:00
Junyoung Choi
9556417447 Merge pull request #2923 from LuisReinoso/master
Add Wakatime integration
2020-07-20 19:26:53 +09:00
Baptiste Augrain
60fbb7db5d fix unclickable links 2020-07-20 19:25:53 +09:00
Baptiste Augrain
e504f8e63e fix broken nord and vulcan themes 2020-07-20 19:25:27 +09:00
Baptiste Augrain
071ce12a7e add updated yarn.lock 2020-07-20 19:23:25 +09:00
Baptiste Augrain
0decaf187c update mermaid due to missing arrowheads 2020-07-20 19:23:25 +09:00
Junyoung Choi
961644747e Update readme.md 2020-07-09 13:28:02 +09:00
ZeroX-DG
d1a81984fb fixed eslint 2020-07-02 11:46:13 +12:00
Baptiste Augrain
bd9b1306b1 fix missing isStacking flag 2020-06-26 17:55:16 +02:00
Baptiste Augrain
0ca18d8ca5 re-add missing isStacking flag 2020-06-26 03:07:25 +02:00
Baptiste Augrain
540d72696c Merge branch 'master' into fix-autocomplete-codeblock 2020-06-26 02:40:37 +02:00
Baptiste Augrain
87a530612f - use newest test
- remove useless binding
- regroup function
2020-06-25 22:50:08 +02:00
ehhc
7f3fdedb5d Update readme.md 2020-06-25 10:03:02 +09:00
ehhc
3a80706938 Update readme.md
Added link to a project developing a mobile version of boostnote
2020-06-25 10:03:02 +09:00
Baptiste Augrain
f4259bb4d0 fix exporting storage's notes into their own folders 2020-06-12 17:36:46 +02:00
Baptiste Augrain
aa8b589569 don't show export menu to snippet notes 2020-06-12 16:56:07 +02:00
Baptiste Augrain
febc98c101 fix exporting storage's notes as PDFs 2020-06-12 16:51:35 +02:00
Baptiste Augrain
b678c3bd89 export html is escaping html characters as the preview 2020-06-12 16:40:16 +02:00
Baptiste Augrain
80b8948433 - export untitled notes as 'Untitled' based on the language
- export notes with duplicate title as '<title> (<index>)'
2020-06-12 16:18:27 +02:00
Baptiste Augrain
5414fe3384 export tag 2020-06-12 15:53:23 +02:00
Baptiste Augrain
db4016385d Merge branch 'master' into export-yfm 2020-06-12 15:17:02 +02:00
ZeroX-DG
2cf5f8e966 updated slack invite link 2020-06-08 18:48:55 +09:00
Luis Reinoso
8ede1a4989 refactor: Change UI according requested changes.
NOTE:
Just a simple section with title Wakatime and bellow is a check box saying enable wakatime? and below it is the text box for wakatime key.
2020-06-07 11:17:02 -05:00
Luis Reinoso
76da76ae76 fix: Wakatime command name. 2020-06-07 11:01:44 -05:00
Luis Reinoso
c02ab033f4 fix: Remove extra calling function. Now call directly to check wakatime plugin. 2020-05-23 12:25:37 -05:00
Luis Reinoso
1aaba74e24 refactor: Improve sendWakatimeHeartBeat. 2020-05-23 12:21:43 -05:00
Luis Reinoso
6fe6794796 feat: Add checkbox validation to active or deactive plugin. 2020-05-16 11:13:19 -05:00
Luis Reinoso
fd3e243855 feat: Check for missing wakatime cli. And display modal and alert message. 2020-05-16 10:08:51 -05:00
Luis Reinoso
938b075bf6 fix: Lint errors. 2020-05-16 08:37:35 -05:00
Baptiste Augrain
81ac3d1748 fix scrolling with only one big paragraph 2020-05-16 14:19:22 +02:00
Baptiste Augrain
40d10eae04 Merge branch 'master' into fix-scroll 2020-05-15 02:59:00 +02:00
Baptiste Augrain
9b6a61a91c fix cursor sync 2020-05-15 02:57:00 +02:00
Baptiste Augrain
7116c305ca Merge branch 'master' into fix-scroll 2020-05-15 02:02:38 +02:00
ZeroX-DG
4fbbb4651d updated Boostnote name to Boostnote Legacy 2020-05-11 03:44:21 +09:00
Luis Reinoso
2ac38e9644 fix: Update prettier config with master. 2020-05-08 17:11:16 -05:00
Luis Reinoso
98d4fa0603 fix: Lint issues. 2020-05-08 10:01:30 -05:00
Luis Reinoso
2ea0514bbe Merge remote-tracking branch 'upstream/master' 2020-05-08 06:55:48 -05:00
Junyoung Choi
137aa692bc Merge pull request #3547 from ZeroX-DG/migrate-to-jest
[WIP] Migrate to jest
2020-05-08 18:49:27 +09:00
ZeroX-DG
634fec39c0 migrate more tests to jest 2020-05-08 20:13:39 +12:00
ZeroX-DG
d269f1e8fd migrate more tests to jest 2020-05-06 14:57:35 +12:00
Junyoung Choi
4b67026bbf Merge pull request #2936 from callumbooth/fix-2903
fixes #2903 - Rearrange layout of columns
2020-04-24 01:04:23 +09:00
Alexander Wolf
8b13ec4f0e Issue 1706 tag rename - finishing #2989 (#3469)
* allow a tag to be renamed and update all notes that use that tag

• repurpose RenameFolderModal.styl to RenameModal so it is more generic

* call handleConfirmButtonClick directly instead of sending through a confirm method

* better name for method to confirm the rename

* use close prop instead of a new method

* use callback ref instead of legacy string refs

* bind the handleChange in the constructor to allow for direct function assignment

* update the tag in the URL upon change

* use the eventEmitter to update the tags in the SnippetNoteDetail header via the TagSelect component

* respect themes when modal is opened

* show error message when trying to rename to an existing tag

* lint fix, const over let

* add missing letter

* fix routing and add merge warning dialog

* fix space-before-parens lint error

* change theming

* add check if tag changed

Co-authored-by: Khaliq Gant <khaliqgant@gmail.com>
2020-04-21 20:55:56 +09:00
Junyoung Choi
7fef7660e4 Add roadmap 2020-04-21 20:45:05 +09:00
Junyoung Choi
01d021cc4c Add boosthub intro 2020-04-21 18:33:57 +09:00
Callum Booth
c355f81525 remove redundant icons 2020-04-20 08:43:01 +01:00
Callum Booth
d138a54dfd fix accidental deletion of rtl state 2020-04-20 08:42:45 +01:00
Junyoung Choi
514d4b9059 v0.15.3 2020-04-20 11:43:38 +09:00
Callum Booth
a7ead67c2d moves orientation button to view menu 2020-04-18 14:17:08 +01:00
Callum Booth
2f16784a20 Merge branch 'master' of https://github.com/BoostIO/Boostnote into fix-2903 2020-04-18 13:54:27 +01:00
ZeroX-DG
8ca3ba21ee Merge branch 'master' of https://github.com/BoostIO/Boostnote into migrate-to-jest 2020-04-17 21:49:38 +12:00
ZeroX-DG
58ae6419f0 fixed markdown test error 2020-04-17 21:43:05 +12:00
Junyoung Choi
b56e0b98e3 Use netral color 2020-04-16 00:55:18 +09:00
Arcturus
4def32ab13 add style rule for table 2020-04-16 00:55:18 +09:00
Arcturus
0de78d12ef add hard coded styling 2020-04-16 00:55:18 +09:00
Arcturus
e756534db4 refactoring 2020-04-16 00:55:18 +09:00
Arcturus
2194965dc4 remove hard coded styling 2020-04-16 00:55:18 +09:00
Arcturus
f9e54bcbfc add styling for code 2020-04-16 00:55:18 +09:00
Junyoung Choi
667fd3a601 Fix test 2020-04-16 00:25:11 +09:00
Junyoung Choi
461e24bf39 Fix regex 2020-04-16 00:25:11 +09:00
Junyoung Choi
ac2cfe5169 Fix themeManager 2020-04-15 23:59:12 +09:00
ZeroX-DG
3f320f4337 resolved conflict 2020-04-12 18:45:49 +12:00
xatier
433ee9ed45 Update zh-TW.json (#3537)
- Update zh-TW translation.
- Sync with [locales/en.json](e44381f295/locales/en.json).
2020-04-09 11:58:35 +09:00
hikerpig
6ee92588b1 When storage or folder is removed, Detail components should render without error (#3168)
* optimize: when storage or folder is removed, Detail components should render without error, fix #2876

* optimize: Handle some scenarios where storage is not found, should not break the renderer

* optimize: NoteList should work without error when storage is not found
2020-04-06 18:02:52 +09:00
Junyoung Choi
0d797ce8a8 Merge pull request #2658 from gregueiras/fixIssue2534
Fix issue2534
2020-04-06 17:51:45 +09:00
Gonçalo Santos
4915c545d9 Merge branch 'master' into fixIssue2534 2020-03-27 01:47:02 +00:00
Gonçalo Santos
e1c95fb1f2 Fix Saving Configuration Bug 2020-03-27 01:42:18 +00:00
Junyoung Choi
5f56d3e0de v0.15.2 2020-03-26 18:32:15 +09:00
Junyoung Choi
d6b86b902c Fix scroll sync (#3531)
* Discard empty file

* Fix scroll sync
2020-03-26 18:02:53 +09:00
dependabot[bot]
3abc0fec38 Bump lodash.mergewith from 4.6.1 to 4.6.2
Bumps [lodash.mergewith](https://github.com/lodash/lodash) from 4.6.1 to 4.6.2.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-26 14:56:36 +09:00
dependabot[bot]
c0619eb746 Bump lodash-es from 4.17.10 to 4.17.15
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.10 to 4.17.15.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.10...4.17.15)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-26 14:55:57 +09:00
AWolf81
791ababe1e add gfm with modified regex & improve link text handling 2020-03-26 03:55:23 +09:00
AWolf81
d829216c8d Remove duplicated if 2020-03-26 03:55:23 +09:00
AWolf81
1cf6f3b1e2 WIP: Change space before parens. Tag link handling issue present. 2020-03-26 03:55:23 +09:00
AWolf81
4d5939aaf4 address requested changes - tag link & redundant line 2020-03-26 03:55:23 +09:00
AWolf81
2695f62f3e add special link handling 2020-03-26 03:55:23 +09:00
KZ
ccd0355d0b Merge pull request #3521 from BoostIO/update-readme
Update readme
2020-03-18 12:48:04 +09:00
KZ
6d6e3a51c0 Update readme 2020-03-18 12:45:43 +09:00
Gonçalo Santos
48c8164689 Fix scheduled theme change timing 2020-03-10 15:41:34 +00:00
Gonçalo Santos
38ed5b8541 Remove handleSlider 2020-03-05 16:34:04 +00:00
Gonçalo Santos
8a6df8bf95 Remove comment 2020-03-05 16:07:45 +00:00
Junyoung Choi
050b1563df v0.15.1 2020-03-04 05:51:13 +09:00
Junyoung Choi
dbbcf385b1 Discard unused ref 2020-03-04 05:50:23 +09:00
Junyoung Choi
d95a3af667 Fix propTypes warning 2020-03-04 05:50:04 +09:00
Junyoung Choi
87a737babc Make rtl optional 2020-03-04 05:43:34 +09:00
Junyoung Choi
a27ddd7490 Fix ref 2020-03-04 05:43:34 +09:00
dependabot[bot]
5693b6d0f5 Bump url-parse from 1.4.0 to 1.4.7
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.4.0 to 1.4.7.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.4.0...1.4.7)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-04 03:56:54 +09:00
dependabot[bot]
9debe8218d Bump lodash.template from 4.4.0 to 4.5.0
Bumps [lodash.template](https://github.com/lodash/lodash) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.4.0...4.5.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-04 03:56:42 +09:00
Aaron Bird
9549355ab7 Tooltip misplaced (#3499)
* fix bug: tooltip misplaced

* Transition only opacity attributes
2020-03-04 03:56:31 +09:00
Nguyen Viet Hung
88e8d2e009 updated PR template (#3498)
* updated PR template

* fixed grammar
2020-02-28 11:53:27 +09:00
Flexo013
a2ea5dd12e Update issue template (#3489)
* Update issue template

* Capitals

* Replace ! with .
2020-02-28 11:52:56 +09:00
Junyoung Choi
9f932a0911 Merge pull request #3002 from AWolf81/feature-tag-links
Add tag link handling with :tag:#tag syntax
2020-02-26 17:32:05 +09:00
Aaron-Bird
71f05b9886 fix: Folder sidebar cannot scroll 2020-02-26 09:57:07 +09:00
AWolf81
2b4e2638dc change tag link format to :tag:tag 2020-02-25 08:16:52 +01:00
Gonçalo Santos
9c3f34fe04 Fix Lint Errors 2020-02-25 03:34:34 +00:00
Gonçalo Santos
d4123eeccd Merge branch 'master' of https://github.com/BoostIO/Boostnote into fixIssue2534 2020-02-25 03:31:21 +00:00
AWolf81
e55f1e0308 fix linting 2020-02-19 00:29:54 +01:00
Alexander Wolf
1d570df129 Merge branch 'master' into feature-tag-links 2020-02-12 20:51:59 +01:00
AWolf81
d125bd07f7 Merge branch 'master' into feature-tag-links 2020-02-12 20:42:09 +01:00
Gonçalo Santos
b91a76b3b1 Merge branch 'master' into fixIssue2534 2019-11-22 00:19:56 +00:00
amedora
fc08d2f8c3 Merge branch 'master' into migrate-to-jest
# Conflicts:
#	tests/lib/snapshots/markdown-test.js.md
#	tests/lib/snapshots/markdown-test.js.snap
2019-10-14 07:41:19 +09:00
Callum Booth
59e361cb37 move config value into ui 2019-10-09 12:59:53 +01:00
Callum Booth
1993a6588d Cleans up toggle button component 2019-10-09 12:45:52 +01:00
Callum Booth
218fba1aa1 fix formatting 2019-10-09 12:41:26 +01:00
Callum Booth
4de6c69f5d merge from upstream/master 2019-10-08 13:21:56 +01:00
lockee14
2b3538d3b1 add indentation and closing brace
missing indentation and brace between line 229 - 234:
before:
'Shift-Cmd-/': function (cm) {
        if (global.process.platform !== 'darwin') { return }
      [translateHotkey(hotkey.insertDateTime)]: function (cm) {
        const dateNow = new Date()
        cm.replaceSelection(dateNow.toLocaleString())
},

after:
'Shift-Cmd-/': function (cm) {
        if (global.process.platform !== 'darwin') { return }
        [translateHotkey(hotkey.insertDateTime)]: function (cm) {
          const dateNow = new Date()
          cm.replaceSelection(dateNow.toLocaleString())
        }
},
2019-09-12 08:30:53 +09:00
lockee14
b84f1173b7 add a comma at the end of line 114 2019-09-12 08:15:42 +09:00
lockee14
bdfe8c0445 add a comma at the end of line 73 2019-09-12 08:14:39 +09:00
lockee14
f64d0b35e1 Merge branch 'master' into Insert_Date_ISO_8601_Style 2019-08-30 21:54:38 +09:00
amedora
3921655157 replace the snapshot file 2019-08-07 15:22:27 +09:00
amedora
e4e10d523f fix how handle the storage as context data 2019-08-07 14:52:17 +09:00
amedora
404dddcb86 rename files to suit Jest 2019-08-07 14:28:38 +09:00
amedora
ffb2603485 jest-codemods tests/lib 2019-08-07 14:17:48 +09:00
mehdi
928e0edf4d forgot to remove commented code 2019-07-29 16:24:42 +09:00
mehdi
80a63f7404 refactor: move the config down to editor setting 2019-07-08 19:27:23 +09:00
mehdi
6e45ee6a38 add Date ISO 8601 format 2019-06-24 06:10:31 +09:00
Callum Booth
ba34458feb changes if state to be more readable 2019-06-10 20:27:55 +01:00
Callum Booth
a2fb50a71c adds destructed fontFamily from props 2019-06-10 19:21:33 +01:00
Callum booth
b15a4007ee merge from master 2019-06-10 19:13:58 +01:00
AWolf81
2e380ceb02 use Connected-React-Router to navigate 2019-05-29 08:29:53 +02:00
AWolf81
f26dea2420 Merge branch 'master' into feature-tag-links 2019-05-29 08:11:51 +02:00
AWolf81
5f96e314fd add tag link handling with :tag:#tag syntax 2019-05-11 09:30:10 +02:00
Callum booth
93f0d3c1cf fixes #2903 - Rearrange layout of columns 2019-03-19 20:39:34 +00:00
Luis Reinoso
8ec7d19f30 Remove unnecessary console.log 2019-03-12 22:38:10 -05:00
Luis Reinoso
a0f5a06c73 Add send wakatimeHeartBeat on constructor CodeEditor to fix when change from Markdown to Snippet #2810 2019-03-12 22:14:10 -05:00
Luis Reinoso
39a98e795f Add preferences plugins wakatime key #2810 2019-03-12 22:05:30 -05:00
Luis Reinoso
57705cf41b Add less WakatimeHeartBeat requests #2810 2019-03-12 20:40:27 -05:00
Luis Reinoso
052c70bb38 Add support to snippetNote #2810 2019-03-12 20:38:55 -05:00
Luis Reinoso
6dc88262c9 Add wakatime-plugin #2810 2019-03-12 20:02:24 -05:00
Baptiste Augrain
9d43e34cfa Merge branch 'master' into export-yfm 2018-12-28 00:02:38 +01:00
Baptiste Augrain
12f9b9342d Merge branch 'master' into fix-autocomplete-codeblock 2018-12-27 22:57:05 +01:00
Baptiste Augrain
e76bc72667 - data-line attributes might not be directly under the body
- support checkbox preference `When scrolling, synchronize preview with editor`
2018-12-24 11:44:02 +01:00
Baptiste Augrain
9310e5e86c Merge branch 'master' into fix-scroll 2018-12-24 10:33:47 +01:00
Baptiste Augrain
fa157f6f76 fix lint error 2018-12-15 15:57:37 +01:00
Baptiste Augrain
d6a54b8a26 export diagrams 2018-12-15 15:53:49 +01:00
Baptiste Augrain
9813412c8e add export menu in note list's context menu 2018-12-15 14:55:13 +01:00
Baptiste Augrain
d76b7235db export styles of code blocks 2018-12-15 12:42:39 +01:00
Baptiste Augrain
418a789568 fix export note in with split editor 2018-12-15 10:22:02 +01:00
gregueiras
a19ff6762e Unit tests added 2018-12-13 18:50:02 +00:00
Baptiste Augrain
2d941c3ea3 Merge branch 'master' into export-yfm 2018-12-13 14:35:25 +01:00
Gonçalo Santos
7034e7b620 Delete Slider.styl
I submitted an extra file, it was not necessary
2018-12-12 11:37:59 +00:00
gregueiras
cd53a65c14 Code Style Improvements 2018-11-29 13:58:15 +00:00
gregueiras
8b54f5aa69 Removed colons and semi colons 2018-11-29 12:25:14 +00:00
gregueiras
ceed178061 Inverted thumb order 2018-11-29 12:12:14 +00:00
gregueiras
33662974bf Fixed default theme thumb background 2018-11-29 12:05:14 +00:00
gregueiras
36b97fc6a2 Interface Improved 2018-11-29 11:58:36 +00:00
Baptiste Augrain
540c608cc6 Merge branch 'master' into fix-scroll 2018-11-28 14:01:18 +01:00
Gonçalo Santos
0f8c627474 Fixed checkbox 2018-11-28 12:09:42 +00:00
gregueiras
f38fef23a0 ThemeManager Created 2018-11-28 12:04:33 +00:00
gregueiras
8be0ea64a5 Scheduled Theme default configuration 2018-11-27 16:41:02 +00:00
gregueiras
1419c71ef5 Border added 2018-11-27 10:40:42 +00:00
gregueiras
e13742445e Format 2018-11-26 22:01:43 +00:00
gregueiras
1d21bb1ea3 Values are now saved 2018-11-26 22:00:02 +00:00
gregueiras
aa38b1f859 Multi tab WIP 2018-11-26 20:11:11 +00:00
Baptiste Augrain
e723d4cd59 add tests 2018-11-18 19:10:48 +01:00
Baptiste Augrain
9e770ef357 fix bad conditions 2018-11-15 23:52:42 +01:00
Baptiste Augrain
c796b3b30e add YAML front matter when exporting 2018-11-15 22:48:14 +01:00
Baptiste Augrain
168fe212f5 add preference tab 2018-11-15 03:15:11 +01:00
Baptiste Augrain
87515dbd3f separate autocomplete configuration between markdown and within code blocks 2018-11-10 12:20:51 +01:00
Baptiste Augrain
696c2f29b5 implements better positioning between the editor and the preview in the SplitEditor (particularly with lot of images) 2018-09-17 16:59:27 +02:00
91 changed files with 4923 additions and 1253 deletions

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ node_modules/*
.idea .idea
.vscode .vscode
package-lock.json package-lock.json
config.json

View File

@@ -5,19 +5,19 @@ Let us know what is currently happening.
Please include some **screenshots** with the **developer tools** open (console tab) when you report a bug. Please include some **screenshots** with the **developer tools** open (console tab) when you report a bug.
If your issue is regarding Boostnote mobile, please open an issue in the Boostnote Mobile repo 👉 https://github.com/BoostIO/boostnote-mobile. If your issue is regarding the new Boost Note.next, please open an issue in the new repo 👉 https://github.com/BoostIO/BoostNote.next/issues.
--> -->
# Expected behavior # Expected behavior
<!-- <!--
Let us know what you think should happen! Let us know what you think should happen.
--> -->
# Steps to reproduce # Steps to reproduce
<!-- <!--
Please be thorough, issues we can reproduce are easier to fix! Please be thorough, issues we can reproduce are easier to fix.
--> -->
1. 1.
@@ -26,8 +26,8 @@ Please be thorough, issues we can reproduce are easier to fix!
# Environment # Environment
- Version : - Boostnote version: <!-- 0.x.x -->
- OS Version and name : - OS version and name: <!-- Windows 10 / Ubuntu 18.04 / etc -->
<!-- <!--
Love Boostnote? Please consider supporting us on IssueHunt: Love Boostnote? Please consider supporting us on IssueHunt:

View File

@@ -3,13 +3,16 @@ Before submitting this PR, please make sure that:
- You have read and understand the contributing.md - You have read and understand the contributing.md
- You have checked docs/code_style.md for information on code style - You have checked docs/code_style.md for information on code style
--> -->
## Description ## Description
<!-- <!--
Tell us what your PR does. Tell us what your PR does.
Please attach a screenshot/ video/gif image describing your PR if possible. Please attach a screenshot/ video/gif image describing your PR if possible.
--> -->
## Issue fixed ## Issue fixed
<!-- <!--
Please list out all issue fixed with this PR here. Please list out all issue fixed with this PR here.
--> -->
@@ -20,6 +23,7 @@ your PR will be reviewed faster if we know exactly what it does.
Change :white_circle: to :radio_button: in all the options that apply Change :white_circle: to :radio_button: in all the options that apply
--> -->
## Type of changes ## Type of changes
- :white_circle: Bug fix (Change that fixed an issue) - :white_circle: Bug fix (Change that fixed an issue)
@@ -34,3 +38,5 @@ Change :white_circle: to :radio_button: in all the options that apply
- :white_circle: I have written test for my code and it has been tested - :white_circle: I have written test for my code and it has been tested
- :white_circle: All existing tests have been passed - :white_circle: All existing tests have been passed
- :white_circle: I have attached a screenshot/video to visualize my change if possible - :white_circle: I have attached a screenshot/video to visualize my change if possible
- :white_circle: This PR will modify the UI or affects the UX
- :white_circle: This PR will add/update/delete a keybinding

View File

@@ -21,6 +21,8 @@ const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
import { createTurndownService } from '../lib/turndown' import { createTurndownService } from '../lib/turndown'
import { languageMaps } from '../lib/CMLanguageList' import { languageMaps } from '../lib/CMLanguageList'
import snippetManager from '../lib/SnippetManager' import snippetManager from '../lib/SnippetManager'
import { findStorage } from 'browser/lib/findStorage'
import { sendWakatimeHeartBeat } from 'browser/lib/wakatime-plugin'
import { import {
generateInEditor, generateInEditor,
tocExistsInEditor tocExistsInEditor
@@ -61,7 +63,7 @@ export default class CodeEditor extends React.Component {
this.focusHandler = () => { this.focusHandler = () => {
ipcRenderer.send('editor:focused', true) ipcRenderer.send('editor:focused', true)
} }
const debouncedDeletionOfAttachments = _.debounce( this.debouncedDeletionOfAttachments = _.debounce(
attachmentManagement.deleteAttachmentsNotPresentInNote, attachmentManagement.deleteAttachmentsNotPresentInNote,
30000 30000
) )
@@ -78,7 +80,7 @@ export default class CodeEditor extends React.Component {
this.props.onBlur != null && this.props.onBlur(e) this.props.onBlur != null && this.props.onBlur(e)
const { storageKey, noteKey } = this.props const { storageKey, noteKey } = this.props
if (this.props.deleteUnusedAttachments === true) { if (this.props.deleteUnusedAttachments === true) {
debouncedDeletionOfAttachments( this.debouncedDeletionOfAttachments(
this.editor.getValue(), this.editor.getValue(),
storageKey, storageKey,
noteKey noteKey
@@ -113,6 +115,16 @@ export default class CodeEditor extends React.Component {
this.editorActivityHandler = () => this.handleEditorActivity() this.editorActivityHandler = () => this.handleEditorActivity()
this.turndownService = createTurndownService() this.turndownService = createTurndownService()
// wakatime
const { storageKey, noteKey } = this.props
const storage = findStorage(storageKey)
if (storage)
sendWakatimeHeartBeat(storage.path, noteKey, storage.name, {
isWrite: false,
hasFileChanges: false,
isFileChange: true
})
} }
handleSearch(msg) { handleSearch(msg) {
@@ -158,6 +170,10 @@ export default class CodeEditor extends React.Component {
} }
handleEditorActivity() { handleEditorActivity() {
if (this.props.onCursorActivity) {
this.props.onCursorActivity(this.editor)
}
if (!this.textEditorInterface.transaction) { if (!this.textEditorInterface.transaction) {
this.updateTableEditorState() this.updateTableEditorState()
} }
@@ -219,11 +235,19 @@ export default class CodeEditor extends React.Component {
}, },
[translateHotkey(hotkey.insertDate)]: function(cm) { [translateHotkey(hotkey.insertDate)]: function(cm) {
const dateNow = new Date() const dateNow = new Date()
cm.replaceSelection(dateNow.toLocaleDateString()) if (self.props.dateFormatISO8601) {
cm.replaceSelection(dateNow.toISOString().split('T')[0])
} else {
cm.replaceSelection(dateNow.toLocaleDateString())
}
}, },
[translateHotkey(hotkey.insertDateTime)]: function(cm) { [translateHotkey(hotkey.insertDateTime)]: function(cm) {
const dateNow = new Date() const dateNow = new Date()
cm.replaceSelection(dateNow.toLocaleString()) if (self.props.dateFormatISO8601) {
cm.replaceSelection(dateNow.toISOString())
} else {
cm.replaceSelection(dateNow.toLocaleString())
}
}, },
Enter: 'boostNewLineAndIndentContinueMarkdownList', Enter: 'boostNewLineAndIndentContinueMarkdownList',
'Ctrl-C': cm => { 'Ctrl-C': cm => {
@@ -321,10 +345,18 @@ export default class CodeEditor extends React.Component {
'CodeMirror-lint-markers' 'CodeMirror-lint-markers'
], ],
autoCloseBrackets: { autoCloseBrackets: {
pairs: this.props.matchingPairs, codeBlock: {
triples: this.props.matchingTriples, pairs: this.props.codeBlockMatchingPairs,
explode: this.props.explodingPairs, closeBefore: this.props.codeBlockMatchingCloseBefore,
override: true triples: this.props.codeBlockMatchingTriples,
explode: this.props.codeBlockExplodingPairs
},
markdown: {
pairs: this.props.matchingPairs,
closeBefore: this.props.matchingCloseBefore,
triples: this.props.matchingTriples,
explode: this.props.explodingPairs
}
}, },
extraKeys: this.defaultKeyMap, extraKeys: this.defaultKeyMap,
prettierConfig: this.props.prettierConfig prettierConfig: this.props.prettierConfig
@@ -352,6 +384,7 @@ export default class CodeEditor extends React.Component {
eventEmitter.emit('code:init') eventEmitter.emit('code:init')
this.editor.on('scroll', this.scrollHandler) this.editor.on('scroll', this.scrollHandler)
this.editor.on('cursorActivity', this.editorActivityHandler)
const editorTheme = document.getElementById('editorTheme') const editorTheme = document.getElementById('editorTheme')
editorTheme.addEventListener('load', this.loadStyleHandler) editorTheme.addEventListener('load', this.loadStyleHandler)
@@ -489,7 +522,6 @@ export default class CodeEditor extends React.Component {
}) })
if (this.props.enableTableEditor) { if (this.props.enableTableEditor) {
this.editor.on('cursorActivity', this.editorActivityHandler)
this.editor.on('changes', this.editorActivityHandler) this.editor.on('changes', this.editorActivityHandler)
} }
@@ -548,12 +580,18 @@ export default class CodeEditor extends React.Component {
this.editor.off('paste', this.pasteHandler) this.editor.off('paste', this.pasteHandler)
eventEmitter.off('top:search', this.searchHandler) eventEmitter.off('top:search', this.searchHandler)
this.editor.off('scroll', this.scrollHandler) this.editor.off('scroll', this.scrollHandler)
this.editor.off('cursorActivity', this.editorActivityHandler)
this.editor.off('contextmenu', this.contextMenuHandler) this.editor.off('contextmenu', this.contextMenuHandler)
const editorTheme = document.getElementById('editorTheme') const editorTheme = document.getElementById('editorTheme')
editorTheme.removeEventListener('load', this.loadStyleHandler) editorTheme.removeEventListener('load', this.loadStyleHandler)
spellcheck.setLanguage(null, spellcheck.SPELLCHECK_DISABLED) spellcheck.setLanguage(null, spellcheck.SPELLCHECK_DISABLED)
eventEmitter.off('code:format-table', this.formatTable) eventEmitter.off('code:format-table', this.formatTable)
if (this.props.enableTableEditor) {
this.editor.off('changes', this.editorActivityHandler)
}
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
@@ -629,16 +667,32 @@ export default class CodeEditor extends React.Component {
if ( if (
prevProps.matchingPairs !== this.props.matchingPairs || prevProps.matchingPairs !== this.props.matchingPairs ||
prevProps.matchingCloseBefore !== this.props.matchingCloseBefore ||
prevProps.matchingTriples !== this.props.matchingTriples || prevProps.matchingTriples !== this.props.matchingTriples ||
prevProps.explodingPairs !== this.props.explodingPairs prevProps.explodingPairs !== this.props.explodingPairs ||
prevProps.codeBlockMatchingPairs !== this.props.codeBlockMatchingPairs ||
prevProps.codeBlockMatchingCloseBefore !==
this.props.codeBlockMatchingCloseBefore ||
prevProps.codeBlockMatchingTriples !==
this.props.codeBlockMatchingTriples ||
prevProps.codeBlockExplodingPairs !== this.props.codeBlockExplodingPairs
) { ) {
const bracketObject = { const autoCloseBrackets = {
pairs: this.props.matchingPairs, codeBlock: {
triples: this.props.matchingTriples, pairs: this.props.codeBlockMatchingPairs,
explode: this.props.explodingPairs, closeBefore: this.props.codeBlockMatchingCloseBefore,
override: true triples: this.props.codeBlockMatchingTriples,
explode: this.props.codeBlockExplodingPairs
},
markdown: {
pairs: this.props.matchingPairs,
closeBefore: this.props.matchingCloseBefore,
triples: this.props.matchingTriples,
explode: this.props.explodingPairs
}
} }
this.editor.setOption('autoCloseBrackets', bracketObject)
this.editor.setOption('autoCloseBrackets', autoCloseBrackets)
} }
if (prevProps.enableTableEditor !== this.props.enableTableEditor) { if (prevProps.enableTableEditor !== this.props.enableTableEditor) {
@@ -756,6 +810,8 @@ export default class CodeEditor extends React.Component {
} }
handleChange(editor, changeObject) { handleChange(editor, changeObject) {
this.debouncedDeletionOfAttachments.cancel()
spellcheck.handleChange(editor, changeObject) spellcheck.handleChange(editor, changeObject)
// The current note contains an toc. We'll check for changes on headlines. // The current note contains an toc. We'll check for changes on headlines.
@@ -793,9 +849,23 @@ export default class CodeEditor extends React.Component {
this.updateHighlight(editor, changeObject) this.updateHighlight(editor, changeObject)
this.value = editor.getValue() this.value = editor.getValue()
const { storageKey, noteKey } = this.props
const storage = findStorage(storageKey)
if (this.props.onChange) { if (this.props.onChange) {
this.props.onChange(editor) this.props.onChange(editor)
} }
const isWrite = !!this.props.onChange
const hasFileChanges = isWrite
if (storage) {
sendWakatimeHeartBeat(storage.path, noteKey, storage.name, {
isWrite,
hasFileChanges,
isFileChange: false
})
}
} }
linePossibleContainsHeadline(currentLine) { linePossibleContainsHeadline(currentLine) {
@@ -923,6 +993,16 @@ export default class CodeEditor extends React.Component {
this.restartHighlighting() this.restartHighlighting()
this.editor.on('change', this.changeHandler) this.editor.on('change', this.changeHandler)
this.editor.refresh() this.editor.refresh()
// wakatime
const { storageKey, noteKey } = this.props
const storage = findStorage(storageKey)
if (storage)
sendWakatimeHeartBeat(storage.path, noteKey, storage.name, {
isWrite: false,
hasFileChanges: false,
isFileChange: true
})
} }
setValue(value) { setValue(value) {
@@ -1240,18 +1320,19 @@ export default class CodeEditor extends React.Component {
} }
render() { render() {
const { className, fontSize } = this.props const { className, fontSize, fontFamily, width, height } = this.props
const fontFamily = normalizeEditorFontFamily(this.props.fontFamily) const normalisedFontFamily = normalizeEditorFontFamily(fontFamily)
const width = this.props.width
return ( return (
<div <div
className={className == null ? 'CodeEditor' : `CodeEditor ${className}`} className={className == null ? 'CodeEditor' : `CodeEditor ${className}`}
ref='root' ref='root'
tabIndex='-1' tabIndex='-1'
style={{ style={{
fontFamily, fontFamily: normalisedFontFamily,
fontSize: fontSize, fontSize,
width: width width,
height
}} }}
onDrop={e => this.handleDropImage(e)} onDrop={e => this.handleDropImage(e)}
/> />

View File

@@ -1,3 +1,4 @@
/* eslint-disable camelcase */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
@@ -29,20 +30,23 @@ class MarkdownEditor extends React.Component {
isLocked: props.isLocked isLocked: props.isLocked
} }
this.lockEditorCode = () => this.handleLockEditor() this.lockEditorCode = this.handleLockEditor.bind(this)
this.focusEditor = this.focusEditor.bind(this)
this.previewRef = React.createRef()
} }
componentDidMount() { componentDidMount() {
this.value = this.refs.code.value this.value = this.refs.code.value
eventEmitter.on('editor:lock', this.lockEditorCode) eventEmitter.on('editor:lock', this.lockEditorCode)
eventEmitter.on('editor:focus', this.focusEditor.bind(this)) eventEmitter.on('editor:focus', this.focusEditor)
} }
componentDidUpdate() { componentDidUpdate() {
this.value = this.refs.code.value this.value = this.refs.code.value
} }
componentWillReceiveProps(props) { UNSAFE_componentWillReceiveProps(props) {
if (props.value !== this.props.value) { if (props.value !== this.props.value) {
this.queueRendering(props.value) this.queueRendering(props.value)
} }
@@ -51,7 +55,7 @@ class MarkdownEditor extends React.Component {
componentWillUnmount() { componentWillUnmount() {
this.cancelQueue() this.cancelQueue()
eventEmitter.off('editor:lock', this.lockEditorCode) eventEmitter.off('editor:lock', this.lockEditorCode)
eventEmitter.off('editor:focus', this.focusEditor.bind(this)) eventEmitter.off('editor:focus', this.focusEditor)
} }
focusEditor() { focusEditor() {
@@ -60,6 +64,9 @@ class MarkdownEditor extends React.Component {
status: 'CODE' status: 'CODE'
}, },
() => { () => {
if (this.refs.code == null) {
return
}
this.refs.code.focus() this.refs.code.focus()
} }
) )
@@ -104,7 +111,7 @@ class MarkdownEditor extends React.Component {
if (newStatus === 'CODE') { if (newStatus === 'CODE') {
this.refs.code.focus() this.refs.code.focus()
} else { } else {
this.refs.preview.focus() this.previewRef.current.focus()
} }
eventEmitter.emit('topbar:togglelockbutton', this.state.status) eventEmitter.emit('topbar:togglelockbutton', this.state.status)
@@ -131,8 +138,8 @@ class MarkdownEditor extends React.Component {
status: 'PREVIEW' status: 'PREVIEW'
}, },
() => { () => {
this.refs.preview.focus() this.previewRef.current.focus()
this.refs.preview.scrollToRow(cursorPosition.line) this.previewRef.current.scrollToLine(cursorPosition.line)
} }
) )
eventEmitter.emit('topbar:togglelockbutton', this.state.status) eventEmitter.emit('topbar:togglelockbutton', this.state.status)
@@ -316,6 +323,7 @@ class MarkdownEditor extends React.Component {
storageKey, storageKey,
noteKey, noteKey,
linesHighlighted, linesHighlighted,
getNote,
RTL RTL
} = this.props } = this.props
@@ -358,8 +366,15 @@ class MarkdownEditor extends React.Component {
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
lineWrapping lineWrapping
matchingPairs={config.editor.matchingPairs} matchingPairs={config.editor.matchingPairs}
matchingCloseBefore={config.editor.matchingCloseBefore}
matchingTriples={config.editor.matchingTriples} matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs} explodingPairs={config.editor.explodingPairs}
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
codeBlockMatchingCloseBefore={
config.editor.codeBlockMatchingCloseBefore
}
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
storageKey={storageKey} storageKey={storageKey}
noteKey={noteKey} noteKey={noteKey}
@@ -374,11 +389,13 @@ class MarkdownEditor extends React.Component {
switchPreview={config.editor.switchPreview} switchPreview={config.editor.switchPreview}
enableMarkdownLint={config.editor.enableMarkdownLint} enableMarkdownLint={config.editor.enableMarkdownLint}
customMarkdownLintConfig={config.editor.customMarkdownLintConfig} customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
dateFormatISO8601={config.editor.dateFormatISO8601}
prettierConfig={config.editor.prettierConfig} prettierConfig={config.editor.prettierConfig}
deleteUnusedAttachments={config.editor.deleteUnusedAttachments} deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
RTL={RTL} RTL={RTL}
/> />
<MarkdownPreview <MarkdownPreview
ref={this.previewRef}
styleName={ styleName={
this.state.status === 'PREVIEW' ? 'preview' : 'preview--hide' this.state.status === 'PREVIEW' ? 'preview' : 'preview--hide'
} }
@@ -397,7 +414,6 @@ class MarkdownEditor extends React.Component {
breaks={config.preview.breaks} breaks={config.preview.breaks}
sanitize={config.preview.sanitize} sanitize={config.preview.sanitize}
mermaidHTMLLabel={config.preview.mermaidHTMLLabel} mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
ref='preview'
onContextMenu={e => this.handleContextMenu(e)} onContextMenu={e => this.handleContextMenu(e)}
onDoubleClick={e => this.handleDoubleClick(e)} onDoubleClick={e => this.handleDoubleClick(e)}
tabIndex='0' tabIndex='0'
@@ -411,6 +427,8 @@ class MarkdownEditor extends React.Component {
customCSS={config.preview.customCSS} customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS} allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox} lineThroughCheckbox={config.preview.lineThroughCheckbox}
getNote={getNote}
export={config.export}
onDrop={e => this.handleDropImage(e)} onDrop={e => this.handleDropImage(e)}
RTL={RTL} RTL={RTL}
/> />

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import { connect } from 'react-redux'
import Markdown from 'browser/lib/markdown' import Markdown from 'browser/lib/markdown'
import _ from 'lodash' import _ from 'lodash'
import CodeMirror from 'codemirror' import CodeMirror from 'codemirror'
@@ -17,182 +18,30 @@ import convertModeName from 'browser/lib/convertModeName'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import mdurl from 'mdurl' import mdurl from 'mdurl'
import exportNote from 'browser/main/lib/dataApi/exportNote' import exportNote from 'browser/main/lib/dataApi/exportNote'
import { escapeHtmlCharacters } from 'browser/lib/utils' import formatMarkdown from 'browser/main/lib/dataApi/formatMarkdown'
import formatHTML, {
CSS_FILES,
buildStyle,
getCodeThemeLink,
getStyleParams,
escapeHtmlCharactersInCodeTag
} from 'browser/main/lib/dataApi/formatHTML'
import formatPDF from 'browser/main/lib/dataApi/formatPDF'
import yaml from 'js-yaml' import yaml from 'js-yaml'
import i18n from 'browser/lib/i18n'
import path from 'path'
import { remote, shell } from 'electron'
import attachmentManagement from '../main/lib/dataApi/attachmentManagement'
import filenamify from 'filenamify'
import { render } from 'react-dom' import { render } from 'react-dom'
import Carousel from 'react-image-carousel' import Carousel from 'react-image-carousel'
import { push } from 'connected-react-router'
import ConfigManager from '../main/lib/ConfigManager' import ConfigManager from '../main/lib/ConfigManager'
import uiThemes from 'browser/lib/ui-themes' import uiThemes from 'browser/lib/ui-themes'
import i18n from 'browser/lib/i18n' import { buildMarkdownPreviewContextMenu } from 'browser/lib/contextMenuBuilder'
const { remote, shell } = require('electron')
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder')
.buildMarkdownPreviewContextMenu
const { app } = remote
const path = require('path')
const fileUrl = require('file-url')
const dialog = remote.dialog const dialog = remote.dialog
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const appPath = fileUrl(
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
)
const CSS_FILES = [
`${appPath}/node_modules/katex/dist/katex.min.css`,
`${appPath}/node_modules/codemirror/lib/codemirror.css`,
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
]
/**
* @param {Object} opts
* @param {String} opts.fontFamily
* @param {Numberl} opts.fontSize
* @param {String} opts.codeBlockFontFamily
* @param {String} opts.theme
* @param {Boolean} [opts.lineNumber] Should show line number
* @param {Boolean} [opts.scrollPastEnd]
* @param {Boolean} [opts.allowCustomCSS] Should add custom css
* @param {String} [opts.customCSS] Will be added to bottom, only if `opts.allowCustomCSS` is truthy
* @returns {String}
*/
function buildStyle(opts) {
const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS,
RTL
} = opts
return `
@font-face {
font-family: 'Lato';
src: url('${appPath}/resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
}
@font-face {
font-family: 'Lato';
src: url('${appPath}/resources/fonts/Lato-Black.woff2') format('woff2'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Black.woff') format('woff'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Black.ttf') format('truetype');
font-style: normal;
font-weight: 700;
text-rendering: optimizeLegibility;
}
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff2') format('woff2'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
}
${markdownStyle}
body {
font-family: '${fontFamily.join("','")}';
font-size: ${fontSize}px;
${
scrollPastEnd
? `
padding-bottom: 90vh;
box-sizing: border-box;
`
: ''
}
${RTL ? 'direction: rtl;' : ''}
${RTL ? 'text-align: right;' : ''}
}
@media print {
body {
padding-bottom: initial;
}
}
code {
font-family: '${codeBlockFontFamily.join("','")}';
background-color: rgba(0,0,0,0.04);
text-align: left;
direction: ltr;
}
.lineNumber {
${lineNumber && 'display: block !important;'}
font-family: '${codeBlockFontFamily.join("','")}';
}
.clipboardButton {
color: rgba(147,147,149,0.8);;
fill: rgba(147,147,149,1);;
border-radius: 50%;
margin: 0px 10px;
border: none;
background-color: transparent;
outline: none;
height: 15px;
width: 15px;
cursor: pointer;
}
.clipboardButton:hover {
transition: 0.2s;
color: #939395;
fill: #939395;
background-color: rgba(0,0,0,0.1);
}
h1, h2 {
border: none;
}
h3 {
margin: 1em 0 0.8em;
}
h4, h5, h6 {
margin: 1.1em 0 0.5em;
}
h1 {
padding: 0.2em 0 0.2em;
margin: 1em 0 8px;
}
h2 {
padding: 0.2em 0 0.2em;
margin: 1em 0 0.7em;
}
body p {
white-space: normal;
}
@media print {
body[data-theme="${theme}"] {
color: #000;
background-color: #fff;
}
.clipboardButton {
display: none
}
}
${allowCustomCSS ? customCSS : ''}
`
}
const scrollBarStyle = ` const scrollBarStyle = `
::-webkit-scrollbar { ::-webkit-scrollbar {
${config.get().ui.showScrollBar ? '' : 'display: none;'} ${config.get().ui.showScrollBar ? '' : 'display: none;'}
@@ -224,22 +73,6 @@ const scrollBarDarkStyle = `
} }
` `
const OSX = global.process.platform === 'darwin'
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
if (!OSX) {
defaultFontFamily.unshift('Microsoft YaHei')
defaultFontFamily.unshift('meiryo')
}
const defaultCodeBlockFontFamily = [
'Monaco',
'Menlo',
'Ubuntu Mono',
'Consolas',
'source-code-pro',
'monospace'
]
// return the line number of the line that used to generate the specified element // return the line number of the line that used to generate the specified element
// return -1 if the line is not found // return -1 if the line is not found
function getSourceLineNumberByElement(element) { function getSourceLineNumberByElement(element) {
@@ -252,7 +85,7 @@ function getSourceLineNumberByElement(element) {
return parent.dataset.line !== undefined ? parseInt(parent.dataset.line) : -1 return parent.dataset.line !== undefined ? parseInt(parent.dataset.line) : -1
} }
export default class MarkdownPreview extends React.Component { class MarkdownPreview extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
@@ -353,94 +186,15 @@ export default class MarkdownPreview extends React.Component {
} }
handleSaveAsMd() { handleSaveAsMd() {
this.exportAsDocument('md') this.exportAsDocument('md', formatMarkdown(this.props))
}
htmlContentFormatter(noteContent, exportTasks, targetDir) {
const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS,
RTL
} = this.getStyleParams()
const inlineStyles = buildStyle({
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS,
RTL
})
let body = this.refs.root.contentWindow.document.body.innerHTML
body = attachmentManagement.fixLocalURLS(body, this.props.storagePath)
const files = [this.getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
files.forEach(file => {
if (global.process.platform === 'win32') {
file = file.replace('file:///', '')
} else {
file = file.replace('file://', '')
}
exportTasks.push({
src: file,
dst: 'css'
})
})
let styles = ''
files.forEach(file => {
styles += `<link rel="stylesheet" href="../css/${path.basename(file)}">`
})
return `<html>
<head>
<base href="file://${targetDir}/">
<meta charset="UTF-8">
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
<style id="style">${inlineStyles}</style>
${styles}
</head>
<body>${body}</body>
</html>`
} }
handleSaveAsHtml() { handleSaveAsHtml() {
this.exportAsDocument('html', (noteContent, exportTasks, targetDir) => this.exportAsDocument('html', formatHTML(this.props))
Promise.resolve(
this.htmlContentFormatter(noteContent, exportTasks, targetDir)
)
)
} }
handleSaveAsPdf() { handleSaveAsPdf() {
this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => { this.exportAsDocument('pdf', formatPDF(this.props))
const printout = new remote.BrowserWindow({
show: false,
webPreferences: { webSecurity: false, javascript: false }
})
printout.loadURL(
'data:text/html;charset=UTF-8,' +
this.htmlContentFormatter(noteContent, exportTasks, targetDir)
)
return new Promise((resolve, reject) => {
printout.webContents.on('did-finish-load', () => {
printout.webContents.printToPDF({}, (err, data) => {
if (err) reject(err)
else resolve(data)
printout.destroy()
})
})
})
})
} }
handlePrint() { handlePrint() {
@@ -448,18 +202,21 @@ export default class MarkdownPreview extends React.Component {
} }
exportAsDocument(fileType, contentFormatter) { exportAsDocument(fileType, contentFormatter) {
const note = this.props.getNote()
const options = { const options = {
defaultPath: filenamify(note.title, {
replacement: '_'
}),
filters: [{ name: 'Documents', extensions: [fileType] }], filters: [{ name: 'Documents', extensions: [fileType] }],
properties: ['openFile', 'createDirectory'] properties: ['openFile', 'createDirectory']
} }
dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => { dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
if (filename) { if (filename) {
const content = this.props.value const storagePath = this.props.storagePath
const storage = this.props.storagePath
const nodeKey = this.props.noteKey
exportNote(nodeKey, storage, content, filename, contentFormatter) exportNote(storagePath, note, filename, contentFormatter)
.then(res => { .then(res => {
dialog.showMessageBox(remote.getCurrentWindow(), { dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info', type: 'info',
@@ -490,32 +247,6 @@ export default class MarkdownPreview extends React.Component {
} }
} }
/**
* @description Convert special characters between three ```
* @param {string[]} splitWithCodeTag Array of HTML strings separated by three ```
* @returns {string} HTML in which special characters between three ``` have been converted
*/
escapeHtmlCharactersInCodeTag(splitWithCodeTag) {
for (let index = 0; index < splitWithCodeTag.length; index++) {
const codeTagRequired =
splitWithCodeTag[index] !== '```' && index < splitWithCodeTag.length - 1
if (codeTagRequired) {
splitWithCodeTag.splice(index + 1, 0, '```')
}
}
let inCodeTag = false
let result = ''
for (let content of splitWithCodeTag) {
if (content === '```') {
inCodeTag = !inCodeTag
} else if (inCodeTag) {
content = escapeHtmlCharacters(content)
}
result += content
}
return result
}
getScrollBarStyle() { getScrollBarStyle() {
const { theme } = this.props const { theme } = this.props
@@ -666,47 +397,6 @@ export default class MarkdownPreview extends React.Component {
} }
} }
getStyleParams() {
const {
fontSize,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS,
RTL
} = this.props
let { fontFamily, codeBlockFontFamily } = this.props
fontFamily =
_.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultFontFamily)
: defaultFontFamily
codeBlockFontFamily =
_.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
? codeBlockFontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
return {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS,
RTL
}
}
applyStyle() { applyStyle() {
const { const {
fontFamily, fontFamily,
@@ -719,12 +409,13 @@ export default class MarkdownPreview extends React.Component {
allowCustomCSS, allowCustomCSS,
customCSS, customCSS,
RTL RTL
} = this.getStyleParams() } = getStyleParams(this.props)
this.getWindow().document.getElementById( this.getWindow().document.getElementById(
'codeTheme' 'codeTheme'
).href = this.getCodeThemeLink(codeBlockTheme) ).href = getCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle({
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
fontFamily, fontFamily,
fontSize, fontSize,
codeBlockFontFamily, codeBlockFontFamily,
@@ -734,15 +425,7 @@ export default class MarkdownPreview extends React.Component {
allowCustomCSS, allowCustomCSS,
customCSS, customCSS,
RTL RTL
}) )
}
getCodeThemeLink(name) {
const theme = consts.THEMES.find(theme => theme.name === name)
return theme != null
? theme.path
: `${appPath}/node_modules/codemirror/theme/elegant.css`
} }
rewriteIframe() { rewriteIframe() {
@@ -776,7 +459,7 @@ export default class MarkdownPreview extends React.Component {
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme) this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
if (sanitize === 'NONE') { if (sanitize === 'NONE') {
const splitWithCodeTag = value.split('```') const splitWithCodeTag = value.split('```')
value = this.escapeHtmlCharactersInCodeTag(splitWithCodeTag) value = escapeHtmlCharactersInCodeTag(splitWithCodeTag)
} }
const renderedHTML = this.markdown.render(value) const renderedHTML = this.markdown.render(value)
attachmentManagement.migrateAttachments(value, storagePath, noteKey) attachmentManagement.migrateAttachments(value, storagePath, noteKey)
@@ -839,13 +522,9 @@ export default class MarkdownPreview extends React.Component {
}) })
} }
) )
const opts = {} const opts = {}
// if (this.props.theme === 'dark') {
// opts['font-color'] = '#DDD'
// opts['line-color'] = '#DDD'
// opts['element-color'] = '#DDD'
// opts['fill'] = '#3A404C'
// }
_.forEach( _.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), this.refs.root.contentWindow.document.querySelectorAll('.flowchart'),
el => { el => {
@@ -1068,17 +747,18 @@ export default class MarkdownPreview extends React.Component {
/** /**
* @public * @public
* @param {Number} targetRow * @param {Number} targetLine
*/ */
scrollToRow(targetRow) { scrollToLine(targetLine) {
const blocks = this.getWindow().document.querySelectorAll( const blocks = this.getWindow().document.querySelectorAll(
'body>[data-line]' 'body [data-line]'
) )
for (let index = 0; index < blocks.length; index++) { for (let index = 0; index < blocks.length; index++) {
let block = blocks[index] let block = blocks[index]
const row = parseInt(block.getAttribute('data-line')) const line = parseInt(block.getAttribute('data-line'))
if (row > targetRow || index === blocks.length - 1) {
if (line > targetLine || index === blocks.length - 1) {
block = blocks[index - 1] block = blocks[index - 1]
block != null && this.scrollTo(0, block.offsetTop) block != null && this.scrollTo(0, block.offsetTop)
break break
@@ -1115,7 +795,11 @@ export default class MarkdownPreview extends React.Component {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
const rawHref = e.target.getAttribute('href') const el = e.target.closest('a[href]')
if (!el) return
const rawHref = el.getAttribute('href')
const { dispatch } = this.props
if (!rawHref) return // not checked href because parser will create file://... string for [empty link]() if (!rawHref) return // not checked href because parser will create file://... string for [empty link]()
const parser = document.createElement('a') const parser = document.createElement('a')
@@ -1169,6 +853,13 @@ export default class MarkdownPreview extends React.Component {
return return
} }
const regexIsTagLink = /^:tag:([\w]+)$/
if (regexIsTagLink.test(rawHref)) {
const tag = rawHref.match(regexIsTagLink)[1]
dispatch(push(`/tags/${encodeURIComponent(tag)}`))
return
}
// other case // other case
this.openExternal(href) this.openExternal(href)
} }
@@ -1213,3 +904,10 @@ MarkdownPreview.propTypes = {
smartArrows: PropTypes.bool, smartArrows: PropTypes.bool,
breaks: PropTypes.bool breaks: PropTypes.bool
} }
export default connect(
null,
null,
null,
{ forwardRef: true }
)(MarkdownPreview)

View File

@@ -13,10 +13,77 @@ class MarkdownSplitEditor extends React.Component {
this.value = props.value this.value = props.value
this.focus = () => this.refs.code.focus() this.focus = () => this.refs.code.focus()
this.reload = () => this.refs.code.reload() this.reload = () => this.refs.code.reload()
this.userScroll = true this.userScroll = props.config.preview.scrollSync
this.state = { this.state = {
isSliderFocused: false, isSliderFocused: false,
codeEditorWidthInPercent: 50 codeEditorWidthInPercent: 50,
codeEditorHeightInPercent: 50
}
}
componentDidUpdate(prevProps) {
if (
this.props.config.preview.scrollSync !==
prevProps.config.preview.scrollSync
) {
this.userScroll = this.props.config.preview.scrollSync
}
}
handleCursorActivity(editor) {
if (this.userScroll) {
const previewDoc = _.get(
this,
'refs.preview.refs.root.contentWindow.document'
)
const previewTop = _.get(previewDoc, 'body.scrollTop')
const line = editor.doc.getCursor().line
let top
if (line === 0) {
top = 0
} else {
const blockElements = previewDoc.querySelectorAll('body [data-line]')
const blocks = []
for (const block of blockElements) {
const l = parseInt(block.getAttribute('data-line'))
blocks.push({
line: l,
top: block.offsetTop
})
if (l > line) {
break
}
}
if (blocks.length === 1) {
const block = blockElements[blockElements.length - 1]
blocks.push({
line: editor.doc.size,
top: block.offsetTop + block.offsetHeight
})
}
const i = blocks.length - 1
const ratio =
(blocks[i].top - blocks[i - 1].top) /
(blocks[i].line - blocks[i - 1].line)
const delta = Math.floor(_.get(previewDoc, 'body.clientHeight') / 3)
top =
blocks[i - 1].top +
Math.floor((line - blocks[i - 1].line) * ratio) -
delta
}
this.scrollTo(previewTop, top, y =>
_.set(previewDoc, 'body.scrollTop', y)
)
} }
} }
@@ -29,59 +96,125 @@ class MarkdownSplitEditor extends React.Component {
this.props.onChange(e) this.props.onChange(e)
} }
handleScroll(e) { handleEditorScroll(e) {
if (!this.props.config.preview.scrollSync) return
const previewDoc = _.get(
this,
'refs.preview.refs.root.contentWindow.document'
)
const codeDoc = _.get(this, 'refs.code.editor.doc')
let srcTop, srcHeight, targetTop, targetHeight
if (this.userScroll) { if (this.userScroll) {
if (e.doc) { const previewDoc = _.get(
srcTop = _.get(e, 'doc.scrollTop') this,
srcHeight = _.get(e, 'doc.height') 'refs.preview.refs.root.contentWindow.document'
targetTop = _.get(previewDoc, 'body.scrollTop') )
targetHeight = _.get(previewDoc, 'body.scrollHeight') const codeDoc = _.get(this, 'refs.code.editor.doc')
const from = codeDoc.cm.coordsChar({ left: 0, top: 0 }).line
const to = codeDoc.cm.coordsChar({
left: 0,
top: codeDoc.cm.display.lastWrapHeight * 1.125
}).line
const previewTop = _.get(previewDoc, 'body.scrollTop')
let top
if (from === 0) {
top = 0
} else if (to === codeDoc.lastLine()) {
top =
_.get(previewDoc, 'body.scrollHeight') -
_.get(previewDoc, 'body.clientHeight')
} else { } else {
srcTop = _.get(previewDoc, 'body.scrollTop') const line = from + Math.floor((to - from) / 3)
srcHeight = _.get(previewDoc, 'body.scrollHeight')
targetTop = _.get(codeDoc, 'scrollTop') const blockElements = previewDoc.querySelectorAll('body [data-line]')
targetHeight = _.get(codeDoc, 'height') const blocks = []
for (const block of blockElements) {
const l = parseInt(block.getAttribute('data-line'))
blocks.push({
line: l,
top: block.offsetTop
})
if (l > line) {
break
}
}
if (blocks.length === 1) {
const block = blockElements[blockElements.length - 1]
blocks.push({
line: codeDoc.size,
top: block.offsetTop + block.offsetHeight
})
}
const i = blocks.length - 1
const ratio =
(blocks[i].top - blocks[i - 1].top) /
(blocks[i].line - blocks[i - 1].line)
top =
blocks[i - 1].top + Math.floor((line - blocks[i - 1].line) * ratio)
} }
const distance = (targetHeight * srcTop) / srcHeight - targetTop this.scrollTo(previewTop, top, y =>
const framerate = 1000 / 60 _.set(previewDoc, 'body.scrollTop', y)
const frames = 20 )
const refractory = frames * framerate }
}
this.userScroll = false handlePreviewScroll(e) {
if (this.userScroll) {
const previewDoc = _.get(
this,
'refs.preview.refs.root.contentWindow.document'
)
const codeDoc = _.get(this, 'refs.code.editor.doc')
let frame = 0 const srcTop = _.get(previewDoc, 'body.scrollTop')
let scrollPos, time const editorTop = _.get(codeDoc, 'scrollTop')
const timer = setInterval(() => {
time = frame / frames let top
scrollPos = if (srcTop === 0) {
time < 0.5 top = 0
? 2 * time * time // ease in } else {
: -1 + (4 - 2 * time) * time // ease out const delta = Math.floor(_.get(previewDoc, 'body.clientHeight') / 3)
if (e.doc) const previewTop = srcTop + delta
_.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
else const blockElements = previewDoc.querySelectorAll('body [data-line]')
_.get(this, 'refs.code.editor').scrollTo( const blocks = []
0, for (const block of blockElements) {
targetTop + scrollPos * distance const top = block.offsetTop
)
if (frame >= frames) { blocks.push({
clearInterval(timer) line: parseInt(block.getAttribute('data-line')),
setTimeout(() => { top
this.userScroll = true })
}, refractory)
if (top > previewTop) {
break
}
} }
frame++
}, framerate) if (blocks.length === 1) {
const block = blockElements[blockElements.length - 1]
blocks.push({
line: codeDoc.size,
top: block.offsetTop + block.offsetHeight
})
}
const i = blocks.length - 1
const from = codeDoc.cm.heightAtLine(blocks[i - 1].line, 'local')
const to = codeDoc.cm.heightAtLine(blocks[i].line, 'local')
const ratio =
(previewTop - blocks[i - 1].top) / (blocks[i].top - blocks[i - 1].top)
top = from + Math.floor((to - from) * ratio) - delta
}
this.scrollTo(editorTop, top, y => codeDoc.cm.scrollTo(0, y))
} }
} }
@@ -114,22 +247,42 @@ class MarkdownSplitEditor extends React.Component {
handleMouseMove(e) { handleMouseMove(e) {
if (this.state.isSliderFocused) { if (this.state.isSliderFocused) {
const rootRect = this.refs.root.getBoundingClientRect() const rootRect = this.refs.root.getBoundingClientRect()
const rootWidth = rootRect.width if (this.props.isStacking) {
const offset = rootRect.left const rootHeight = rootRect.height
let newCodeEditorWidthInPercent = ((e.pageX - offset) / rootWidth) * 100 const offset = rootRect.top
let newCodeEditorHeightInPercent =
((e.pageY - offset) / rootHeight) * 100
// limit minSize to 10%, maxSize to 90% // limit minSize to 10%, maxSize to 90%
if (newCodeEditorWidthInPercent <= 10) { if (newCodeEditorHeightInPercent <= 10) {
newCodeEditorWidthInPercent = 10 newCodeEditorHeightInPercent = 10
}
if (newCodeEditorHeightInPercent >= 90) {
newCodeEditorHeightInPercent = 90
}
this.setState({
codeEditorHeightInPercent: newCodeEditorHeightInPercent
})
} else {
const rootWidth = rootRect.width
const offset = rootRect.left
let newCodeEditorWidthInPercent = ((e.pageX - offset) / rootWidth) * 100
// limit minSize to 10%, maxSize to 90%
if (newCodeEditorWidthInPercent <= 10) {
newCodeEditorWidthInPercent = 10
}
if (newCodeEditorWidthInPercent >= 90) {
newCodeEditorWidthInPercent = 90
}
this.setState({
codeEditorWidthInPercent: newCodeEditorWidthInPercent
})
} }
if (newCodeEditorWidthInPercent >= 90) {
newCodeEditorWidthInPercent = 90
}
this.setState({
codeEditorWidthInPercent: newCodeEditorWidthInPercent
})
} }
} }
@@ -147,6 +300,35 @@ class MarkdownSplitEditor extends React.Component {
}) })
} }
scrollTo(from, to, scroller) {
const distance = to - from
const framerate = 1000 / 60
const frames = 20
const refractory = frames * framerate
this.userScroll = false
let frame = 0
let scrollPos, time
const timer = setInterval(() => {
time = frame / frames
scrollPos =
time < 0.5
? 2 * time * time // ease in
: -1 + (4 - 2 * time) * time // ease out
scroller(from + scrollPos * distance)
if (frame >= frames) {
clearInterval(timer)
setTimeout(() => {
this.userScroll = true
}, refractory)
}
frame++
}, framerate)
}
render() { render() {
const { const {
config, config,
@@ -154,17 +336,72 @@ class MarkdownSplitEditor extends React.Component {
storageKey, storageKey,
noteKey, noteKey,
linesHighlighted, linesHighlighted,
getNote,
isStacking,
RTL RTL
} = this.props } = this.props
const storage = findStorage(storageKey) let storage
try {
storage = findStorage(storageKey)
} catch (e) {
return <div />
}
let editorStyle = {}
let previewStyle = {}
let sliderStyle = {}
let editorFontSize = parseInt(config.editor.fontSize, 10) let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
editorStyle.fontSize = editorFontSize
let editorIndentSize = parseInt(config.editor.indentSize, 10) let editorIndentSize = parseInt(config.editor.indentSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4 if (!(editorStyle.fontSize > 0 && editorStyle.fontSize < 132))
const previewStyle = {} editorIndentSize = 4
previewStyle.width = 100 - this.state.codeEditorWidthInPercent + '%' editorStyle.indentSize = editorIndentSize
editorStyle = Object.assign(
editorStyle,
isStacking
? {
width: '100%',
height: `${this.state.codeEditorHeightInPercent}%`
}
: {
width: `${this.state.codeEditorWidthInPercent}%`,
height: '100%'
}
)
previewStyle = Object.assign(
previewStyle,
isStacking
? {
width: '100%',
height: `${100 - this.state.codeEditorHeightInPercent}%`
}
: {
width: `${100 - this.state.codeEditorWidthInPercent}%`,
height: '100%'
}
)
sliderStyle = Object.assign(
sliderStyle,
isStacking
? {
left: 0,
top: `${this.state.codeEditorHeightInPercent}%`
}
: {
left: `${this.state.codeEditorWidthInPercent}%`,
top: 0
}
)
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused) if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused)
previewStyle.pointerEvents = 'none' previewStyle.pointerEvents = 'none'
return ( return (
<div <div
styleName='root' styleName='root'
@@ -174,20 +411,28 @@ class MarkdownSplitEditor extends React.Component {
> >
<CodeEditor <CodeEditor
ref='code' ref='code'
width={this.state.codeEditorWidthInPercent + '%'} width={editorStyle.width}
height={editorStyle.height}
mode='Boost Flavored Markdown' mode='Boost Flavored Markdown'
value={value} value={value}
theme={config.editor.theme} theme={config.editor.theme}
keyMap={config.editor.keyMap} keyMap={config.editor.keyMap}
fontFamily={config.editor.fontFamily} fontFamily={config.editor.fontFamily}
fontSize={editorFontSize} fontSize={editorStyle.fontSize}
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
lineWrapping lineWrapping
matchingPairs={config.editor.matchingPairs} matchingPairs={config.editor.matchingPairs}
matchingCloseBefore={config.editor.matchingCloseBefore}
matchingTriples={config.editor.matchingTriples} matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs} explodingPairs={config.editor.explodingPairs}
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
codeBlockMatchingCloseBefore={
config.editor.codeBlockMatchingCloseBefore
}
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
indentType={config.editor.indentType} indentType={config.editor.indentType}
indentSize={editorIndentSize} indentSize={editorStyle.indentSize}
enableRulers={config.editor.enableRulers} enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers} rulers={config.editor.rulers}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
@@ -197,24 +442,27 @@ class MarkdownSplitEditor extends React.Component {
noteKey={noteKey} noteKey={noteKey}
linesHighlighted={linesHighlighted} linesHighlighted={linesHighlighted}
onChange={e => this.handleOnChange(e)} onChange={e => this.handleOnChange(e)}
onScroll={this.handleScroll.bind(this)} onScroll={e => this.handleEditorScroll(e)}
onCursorActivity={e => this.handleCursorActivity(e)}
spellCheck={config.editor.spellcheck} spellCheck={config.editor.spellcheck}
enableSmartPaste={config.editor.enableSmartPaste} enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey} hotkey={config.hotkey}
switchPreview={config.editor.switchPreview} switchPreview={config.editor.switchPreview}
enableMarkdownLint={config.editor.enableMarkdownLint} enableMarkdownLint={config.editor.enableMarkdownLint}
customMarkdownLintConfig={config.editor.customMarkdownLintConfig} customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
dateFormatISO8601={config.editor.dateFormatISO8601}
deleteUnusedAttachments={config.editor.deleteUnusedAttachments} deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
RTL={RTL} RTL={RTL}
/> />
<div <div
styleName='slider' styleName={isStacking ? 'slider-hoz' : 'slider'}
style={{ left: this.state.codeEditorWidthInPercent + '%' }} style={{ left: sliderStyle.left, top: sliderStyle.top }}
onMouseDown={e => this.handleMouseDown(e)} onMouseDown={e => this.handleMouseDown(e)}
> >
<div styleName='slider-hitbox' /> <div styleName='slider-hitbox' />
</div> </div>
<MarkdownPreview <MarkdownPreview
ref='preview'
style={previewStyle} style={previewStyle}
theme={config.ui.theme} theme={config.ui.theme}
keyMap={config.editor.keyMap} keyMap={config.editor.keyMap}
@@ -223,23 +471,25 @@ class MarkdownSplitEditor extends React.Component {
codeBlockTheme={config.preview.codeBlockTheme} codeBlockTheme={config.preview.codeBlockTheme}
codeBlockFontFamily={config.editor.fontFamily} codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber} lineNumber={config.preview.lineNumber}
indentSize={editorIndentSize}
scrollPastEnd={config.preview.scrollPastEnd} scrollPastEnd={config.preview.scrollPastEnd}
smartQuotes={config.preview.smartQuotes} smartQuotes={config.preview.smartQuotes}
smartArrows={config.preview.smartArrows} smartArrows={config.preview.smartArrows}
breaks={config.preview.breaks} breaks={config.preview.breaks}
sanitize={config.preview.sanitize} sanitize={config.preview.sanitize}
mermaidHTMLLabel={config.preview.mermaidHTMLLabel} mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
ref='preview'
tabInde='0' tabInde='0'
value={value} value={value}
onCheckboxClick={e => this.handleCheckboxClick(e)} onCheckboxClick={e => this.handleCheckboxClick(e)}
onScroll={this.handleScroll.bind(this)} onScroll={e => this.handlePreviewScroll(e)}
showCopyNotification={config.ui.showCopyNotification} showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path} storagePath={storage.path}
noteKey={noteKey} noteKey={noteKey}
customCSS={config.preview.customCSS} customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS} allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox} lineThroughCheckbox={config.preview.lineThroughCheckbox}
getNote={getNote}
export={config.export}
RTL={RTL} RTL={RTL}
/> />
</div> </div>

View File

@@ -3,6 +3,7 @@
height 100% height 100%
font-size 30px font-size 30px
display flex display flex
flex-wrap wrap
.slider .slider
absolute top bottom absolute top bottom
top -2px top -2px
@@ -15,6 +16,14 @@
left -3px left -3px
z-index 10 z-index 10
cursor col-resize cursor col-resize
.slider-hoz
absolute left right
.slider-hitbox
absolute left right
width: 100%
height 7px
cursor row-resize
apply-theme(theme) apply-theme(theme)
body[data-theme={theme}] body[data-theme={theme}]

View File

@@ -21,7 +21,9 @@ const FolderIcon = ({ className, color, isActive }) => {
/** /**
* @param {boolean} isActive * @param {boolean} isActive
* @param {object} tooltipRef,
* @param {Function} handleButtonClick * @param {Function} handleButtonClick
* @param {Function} handleMouseEnter
* @param {Function} handleContextMenu * @param {Function} handleContextMenu
* @param {string} folderName * @param {string} folderName
* @param {string} folderColor * @param {string} folderColor
@@ -35,7 +37,9 @@ const FolderIcon = ({ className, color, isActive }) => {
const StorageItem = ({ const StorageItem = ({
styles, styles,
isActive, isActive,
tooltipRef,
handleButtonClick, handleButtonClick,
handleMouseEnter,
handleContextMenu, handleContextMenu,
folderName, folderName,
folderColor, folderColor,
@@ -49,6 +53,7 @@ const StorageItem = ({
<button <button
styleName={isActive ? 'folderList-item--active' : 'folderList-item'} styleName={isActive ? 'folderList-item--active' : 'folderList-item'}
onClick={handleButtonClick} onClick={handleButtonClick}
onMouseEnter={handleMouseEnter}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
onDrop={handleDrop} onDrop={handleDrop}
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
@@ -75,7 +80,9 @@ const StorageItem = ({
<span styleName='folderList-item-noteCount'>{noteCount}</span> <span styleName='folderList-item-noteCount'>{noteCount}</span>
)} )}
{isFolded && ( {isFolded && (
<span styleName='folderList-item-tooltip'>{folderName}</span> <span styleName='folderList-item-tooltip' ref={tooltipRef}>
{folderName}
</span>
)} )}
</button> </button>
) )
@@ -83,7 +90,9 @@ const StorageItem = ({
StorageItem.propTypes = { StorageItem.propTypes = {
isActive: PropTypes.bool.isRequired, isActive: PropTypes.bool.isRequired,
tooltipRef: PropTypes.object,
handleButtonClick: PropTypes.func, handleButtonClick: PropTypes.func,
handleMouseEnter: PropTypes.func,
handleContextMenu: PropTypes.func, handleContextMenu: PropTypes.func,
folderName: PropTypes.string.isRequired, folderName: PropTypes.string.isRequired,
folderColor: PropTypes.string, folderColor: PropTypes.string,

View File

@@ -60,6 +60,7 @@
border-bottom-right-radius 2px border-bottom-right-radius 2px
height 34px height 34px
line-height 32px line-height 32px
transition-property opacity
.folderList-item:hover, .folderList-item--active:hover .folderList-item:hover, .folderList-item--active:hover
.folderList-item-tooltip .folderList-item-tooltip

View File

@@ -1,5 +1,7 @@
.storageList .storageList
margin-bottom 37px absolute left right
bottom 37px
top 180px
overflow-y auto overflow-y auto
.storageList-folded .storageList-folded

View File

@@ -1,4 +1,4 @@
import mermaidAPI from 'mermaid' import mermaidAPI from 'mermaid/dist/mermaid.min.js'
import uiThemes from 'browser/lib/ui-themes' import uiThemes from 'browser/lib/ui-themes'
// fixes bad styling in the mermaid dark theme // fixes bad styling in the mermaid dark theme
@@ -61,7 +61,6 @@ function render(element, content, theme, enableHTMLLabel) {
el.setAttribute('ratio', ratio) el.setAttribute('ratio', ratio)
el.setAttribute('height', el.parentNode.clientWidth / ratio) el.setAttribute('height', el.parentNode.clientWidth / ratio)
console.log(el)
} }
}) })
} catch (e) { } catch (e) {

View File

@@ -8,7 +8,7 @@ import markdownItTocAndAnchor from '@hikerpig/markdown-it-toc-and-anchor'
import _ from 'lodash' import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import katex from 'katex' import katex from 'katex'
import { lastFindInArray } from './utils' import { escapeHtmlCharacters, lastFindInArray } from './utils'
function createGutter(str, firstLineNumber) { function createGutter(str, firstLineNumber) {
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1 if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
@@ -31,7 +31,8 @@ class Markdown {
html: true, html: true,
xhtmlOut: true, xhtmlOut: true,
breaks: config.preview.breaks, breaks: config.preview.breaks,
sanitize: 'STRICT' sanitize: 'STRICT',
onFence: () => {}
} }
const updatedOptions = Object.assign(defaultOptions, options) const updatedOptions = Object.assign(defaultOptions, options)
@@ -266,22 +267,26 @@ class Markdown {
token.parameters.format = 'yaml' token.parameters.format = 'yaml'
} }
updatedOptions.onFence('chart', token.parameters.format)
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="chart" data-height="${ <div class="chart" data-height="${
token.parameters.height token.parameters.height
}" data-format="${token.parameters.format || 'json'}">${ }" data-format="${token.parameters.format || 'json'}">${
token.content token.content
}</div> }</div>
</pre>` </pre>`
}, },
flowchart: token => { flowchart: token => {
updatedOptions.onFence('flowchart')
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="flowchart" data-height="${token.parameters.height}">${ <div class="flowchart" data-height="${token.parameters.height}">${
token.content token.content
}</div> }</div>
</pre>` </pre>`
}, },
gallery: token => { gallery: token => {
const content = token.content const content = token.content
@@ -298,35 +303,41 @@ class Markdown {
.join('\n') .join('\n')
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="gallery" data-autoplay="${ <div class="gallery" data-autoplay="${
token.parameters.autoplay token.parameters.autoplay
}" data-height="${token.parameters.height}">${content}</div> }" data-height="${token.parameters.height}">${content}</div>
</pre>` </pre>`
}, },
mermaid: token => { mermaid: token => {
updatedOptions.onFence('mermaid')
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="mermaid" data-height="${token.parameters.height}">${ <div class="mermaid" data-height="${token.parameters.height}">${
token.content token.content
}</div> }</div>
</pre>` </pre>`
}, },
sequence: token => { sequence: token => {
updatedOptions.onFence('sequence')
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="sequence" data-height="${token.parameters.height}">${ <div class="sequence" data-height="${token.parameters.height}">${
token.content token.content
}</div> }</div>
</pre>` </pre>`
} }
}, },
token => { token => {
updatedOptions.onFence('code', token.langType)
return `<pre class="code CodeMirror" data-line="${token.map[0]}"> return `<pre class="code CodeMirror" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
${createGutter(token.content, token.firstLineNumber)} ${createGutter(token.content, token.firstLineNumber)}
<code class="${token.langType}">${token.content}</code> <code class="${token.langType}">${token.content}</code>
</pre>` </pre>`
} }
) )
@@ -468,6 +479,16 @@ class Markdown {
return true return true
}) })
this.md.renderer.rules.code_inline = function(tokens, idx) {
const token = tokens[idx]
return (
'<code class="inline">' +
escapeHtmlCharacters(token.content) +
'</code>'
)
}
if (config.preview.smartArrows) { if (config.preview.smartArrows) {
this.md.use(smartArrows) this.md.use(smartArrows)
} }

View File

@@ -0,0 +1,49 @@
import config from 'browser/main/lib/ConfigManager'
const exec = require('child_process').exec
const path = require('path')
let lastHeartbeat = 0
function sendWakatimeHeartBeat(
storagePath,
noteKey,
storageName,
{ isWrite, hasFileChanges, isFileChange }
) {
if (
config.get().wakatime.isActive &&
!!config.get().wakatime.key &&
(new Date().getTime() - lastHeartbeat > 120000 || isFileChange)
) {
const notePath = path.join(storagePath, 'notes', noteKey + '.cson')
if (!isWrite && !hasFileChanges && !isFileChange) {
return
}
lastHeartbeat = new Date()
const wakatimeKey = config.get().wakatime.key
if (wakatimeKey) {
exec(
`wakatime --file ${notePath} --project '${storageName}' --key ${wakatimeKey} --plugin Boostnote-wakatime`,
(error, stdOut, stdErr) => {
if (error) {
console.log(error)
lastHeartbeat = 0
} else {
console.log(
'wakatime',
'isWrite',
isWrite,
'hasChanges',
hasFileChanges,
'isFileChange',
isFileChange
)
}
}
)
}
}
}
export { sendWakatimeHeartBeat }

View File

@@ -294,7 +294,7 @@ class FolderSelect extends React.Component {
{optionList} {optionList}
</div> </div>
</div> </div>
) : ( ) : currentOption ? (
<div styleName='idle' style={{ color: currentOption.folder.color }}> <div styleName='idle' style={{ color: currentOption.folder.color }}>
<div styleName='idle-label'> <div styleName='idle-label'>
<i className='fa fa-folder' /> <i className='fa fa-folder' />
@@ -303,7 +303,7 @@ class FolderSelect extends React.Component {
</span> </span>
</div> </div>
</div> </div>
)} ) : null}
</div> </div>
) )
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable camelcase */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
@@ -56,8 +57,11 @@ class MarkdownNoteDetail extends React.Component {
this.dispatchTimer = null this.dispatchTimer = null
this.generateToc = this.handleGenerateToc.bind(this)
this.toggleLockButton = this.handleToggleLockButton.bind(this) this.toggleLockButton = this.handleToggleLockButton.bind(this)
this.generateToc = () => this.handleGenerateToc() this.handleUpdateContent = this.handleUpdateContent.bind(this)
this.handleSwitchStackDirection = this.handleSwitchStackDirection.bind(this)
this.getNote = this.getNote.bind(this)
} }
focus() { focus() {
@@ -65,6 +69,7 @@ class MarkdownNoteDetail extends React.Component {
} }
componentDidMount() { componentDidMount() {
ee.on('editor:orientation', this.handleSwitchStackDirection)
ee.on('topbar:togglelockbutton', this.toggleLockButton) ee.on('topbar:togglelockbutton', this.toggleLockButton)
ee.on('topbar:toggledirectionbutton', () => this.handleSwitchDirection()) ee.on('topbar:toggledirectionbutton', () => this.handleSwitchDirection())
ee.on('topbar:togglemodebutton', () => { ee.on('topbar:togglemodebutton', () => {
@@ -76,7 +81,7 @@ class MarkdownNoteDetail extends React.Component {
ee.on('code:generate-toc', this.generateToc) ee.on('code:generate-toc', this.generateToc)
} }
componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
const isNewNote = nextProps.note.key !== this.props.note.key const isNewNote = nextProps.note.key !== this.props.note.key
const hasDeletedTags = const hasDeletedTags =
nextProps.note.tags.length < this.props.note.tags.length nextProps.note.tags.length < this.props.note.tags.length
@@ -381,7 +386,7 @@ class MarkdownNoteDetail extends React.Component {
handleSwitchMode(type) { handleSwitchMode(type) {
// If in split mode, hide the lock button // If in split mode, hide the lock button
this.setState( this.setState(
{ editorType: type, isLockButtonShown: !(type === 'SPLIT') }, { editorType: type, isLockButtonShown: type !== 'SPLIT' },
() => { () => {
this.focus() this.focus()
const newConfig = Object.assign({}, this.props.config) const newConfig = Object.assign({}, this.props.config)
@@ -391,7 +396,22 @@ class MarkdownNoteDetail extends React.Component {
) )
} }
handleSwitchStackDirection() {
this.setState(
prevState => ({ isStacking: !prevState.isStacking }),
() => {
this.focus()
const newConfig = Object.assign({}, this.props.config)
newConfig.ui.isStacking = this.state.isStacking
ConfigManager.set(newConfig)
}
)
}
handleSwitchDirection() { handleSwitchDirection() {
if (!this.props.config.editor.rtlEnabled) {
return
}
// If in split mode, hide the lock button // If in split mode, hide the lock button
const direction = this.state.RTL const direction = this.state.RTL
this.setState({ RTL: !direction }) this.setState({ RTL: !direction })
@@ -422,9 +442,13 @@ class MarkdownNoteDetail extends React.Component {
this.updateNote(note) this.updateNote(note)
} }
getNote() {
return this.state.note
}
renderEditor() { renderEditor() {
const { config, ignorePreviewPointerEvents } = this.props const { config, ignorePreviewPointerEvents } = this.props
const { note } = this.state const { note, isStacking } = this.state
if (this.state.editorType === 'EDITOR_PREVIEW') { if (this.state.editorType === 'EDITOR_PREVIEW') {
return ( return (
@@ -436,10 +460,10 @@ class MarkdownNoteDetail extends React.Component {
storageKey={note.storage} storageKey={note.storage}
noteKey={note.key} noteKey={note.key}
linesHighlighted={note.linesHighlighted} linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)} onChange={this.handleUpdateContent}
isLocked={this.state.isLocked}
ignorePreviewPointerEvents={ignorePreviewPointerEvents} ignorePreviewPointerEvents={ignorePreviewPointerEvents}
RTL={this.state.RTL} getNote={this.getNote}
RTL={config.editor.rtlEnabled && this.state.RTL}
/> />
) )
} else { } else {
@@ -450,10 +474,12 @@ class MarkdownNoteDetail extends React.Component {
value={note.content} value={note.content}
storageKey={note.storage} storageKey={note.storage}
noteKey={note.key} noteKey={note.key}
isStacking={isStacking}
linesHighlighted={note.linesHighlighted} linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)} onChange={this.handleUpdateContent}
ignorePreviewPointerEvents={ignorePreviewPointerEvents} ignorePreviewPointerEvents={ignorePreviewPointerEvents}
RTL={this.state.RTL} getNote={this.getNote}
RTL={config.editor.rtlEnabled && this.state.RTL}
/> />
) )
} }
@@ -474,10 +500,16 @@ class MarkdownNoteDetail extends React.Component {
}) })
}) })
}) })
const currentOption = options.filter(
const currentOption = _.find(
options,
option => option =>
option.storage.key === storageKey && option.folder.key === folderKey option.storage.key === storageKey && option.folder.key === folderKey
)[0] )
// currentOption may be undefined
const storageName = _.get(currentOption, 'storage.name') || ''
const folderName = _.get(currentOption, 'folder.name') || ''
const trashTopBar = ( const trashTopBar = (
<div styleName='info'> <div styleName='info'>
@@ -490,8 +522,8 @@ class MarkdownNoteDetail extends React.Component {
/> />
<InfoButton onClick={e => this.handleInfoButtonClick(e)} /> <InfoButton onClick={e => this.handleInfoButtonClick(e)} />
<InfoPanelTrashed <InfoPanelTrashed
storageName={currentOption.storage.name} storageName={storageName}
folderName={currentOption.folder.name} folderName={folderName}
updatedAt={formatDate(note.updatedAt)} updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)} createdAt={formatDate(note.createdAt)}
exportAsHtml={this.exportAsHtml} exportAsHtml={this.exportAsHtml}
@@ -536,10 +568,12 @@ class MarkdownNoteDetail extends React.Component {
onClick={e => this.handleSwitchMode(e)} onClick={e => this.handleSwitchMode(e)}
editorType={editorType} editorType={editorType}
/> />
<ToggleDirectionButton {this.props.config.editor.rtlEnabled && (
onClick={e => this.handleSwitchDirection(e)} <ToggleDirectionButton
isRTL={this.state.RTL} onClick={e => this.handleSwitchDirection(e)}
/> isRTL={this.state.RTL}
/>
)}
<StarButton <StarButton
onClick={e => this.handleStarButtonClick(e)} onClick={e => this.handleStarButtonClick(e)}
isActive={note.isStarred} isActive={note.isStarred}
@@ -572,8 +606,8 @@ class MarkdownNoteDetail extends React.Component {
<InfoButton onClick={e => this.handleInfoButtonClick(e)} /> <InfoButton onClick={e => this.handleInfoButtonClick(e)} />
<InfoPanel <InfoPanel
storageName={currentOption.storage.name} storageName={storageName}
folderName={currentOption.folder.name} folderName={folderName}
noteLink={`[${note.title}](:note:${ noteLink={`[${note.title}](:note:${
queryString.parse(location.search).key queryString.parse(location.search).key
})`} })`}

View File

@@ -859,8 +859,15 @@ class SnippetNoteDetail extends React.Component {
indentSize={editorIndentSize} indentSize={editorIndentSize}
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
matchingPairs={config.editor.matchingPairs} matchingPairs={config.editor.matchingPairs}
matchingCloseBefore={config.editor.matchingCloseBefore}
matchingTriples={config.editor.matchingTriples} matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs} explodingPairs={config.editor.explodingPairs}
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
codeBlockMatchingCloseBefore={
config.editor.codeBlockMatchingCloseBefore
}
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
keyMap={config.editor.keyMap} keyMap={config.editor.keyMap}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
fetchUrlTitle={config.editor.fetchUrlTitle} fetchUrlTitle={config.editor.fetchUrlTitle}
@@ -870,6 +877,9 @@ class SnippetNoteDetail extends React.Component {
enableSmartPaste={config.editor.enableSmartPaste} enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey} hotkey={config.hotkey}
autoDetect={autoDetect} autoDetect={autoDetect}
dateFormatISO8601={config.editor.dateFormatISO8601}
storageKey={storageKey}
noteKey={note.key}
/> />
)} )}
</div> </div>
@@ -885,10 +895,16 @@ class SnippetNoteDetail extends React.Component {
}) })
}) })
}) })
const currentOption = options.filter(
const currentOption = _.find(
options,
option => option =>
option.storage.key === storageKey && option.folder.key === folderKey option.storage.key === storageKey && option.folder.key === folderKey
)[0] )
// currentOption may be undefined
const storageName = _.get(currentOption, 'storage.name') || ''
const folderName = _.get(currentOption, 'folder.name') || ''
const trashTopBar = ( const trashTopBar = (
<div styleName='info'> <div styleName='info'>
@@ -901,8 +917,8 @@ class SnippetNoteDetail extends React.Component {
/> />
<InfoButton onClick={e => this.handleInfoButtonClick(e)} /> <InfoButton onClick={e => this.handleInfoButtonClick(e)} />
<InfoPanelTrashed <InfoPanelTrashed
storageName={currentOption.storage.name} storageName={storageName}
folderName={currentOption.folder.name} folderName={folderName}
updatedAt={formatDate(note.updatedAt)} updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)} createdAt={formatDate(note.createdAt)}
exportAsMd={this.showWarning} exportAsMd={this.showWarning}
@@ -951,8 +967,8 @@ class SnippetNoteDetail extends React.Component {
<InfoButton onClick={e => this.handleInfoButtonClick(e)} /> <InfoButton onClick={e => this.handleInfoButtonClick(e)} />
<InfoPanel <InfoPanel
storageName={currentOption.storage.name} storageName={storageName}
folderName={currentOption.folder.name} folderName={folderName}
noteLink={`[${note.title}](:note:${ noteLink={`[${note.title}](:note:${
queryString.parse(location.search).key queryString.parse(location.search).key
})`} })`}

View File

@@ -20,6 +20,7 @@ class TagSelect extends React.Component {
} }
this.handleAddTag = this.handleAddTag.bind(this) this.handleAddTag = this.handleAddTag.bind(this)
this.handleRenameTag = this.handleRenameTag.bind(this)
this.onInputBlur = this.onInputBlur.bind(this) this.onInputBlur = this.onInputBlur.bind(this)
this.onInputChange = this.onInputChange.bind(this) this.onInputChange = this.onInputChange.bind(this)
this.onInputKeyDown = this.onInputKeyDown.bind(this) this.onInputKeyDown = this.onInputKeyDown.bind(this)
@@ -88,6 +89,7 @@ class TagSelect extends React.Component {
this.buildSuggestions() this.buildSuggestions()
ee.on('editor:add-tag', this.handleAddTag) ee.on('editor:add-tag', this.handleAddTag)
ee.on('sidebar:rename-tag', this.handleRenameTag)
} }
componentDidUpdate() { componentDidUpdate() {
@@ -96,12 +98,23 @@ class TagSelect extends React.Component {
componentWillUnmount() { componentWillUnmount() {
ee.off('editor:add-tag', this.handleAddTag) ee.off('editor:add-tag', this.handleAddTag)
ee.off('sidebar:rename-tag', this.handleRenameTag)
} }
handleAddTag() { handleAddTag() {
this.refs.newTag.input.focus() this.refs.newTag.input.focus()
} }
handleRenameTag(event, tagChange) {
const { value } = this.props
const { tag, updatedTag } = tagChange
const newTags = value.slice()
newTags[value.indexOf(tag)] = updatedTag
this.value = newTags
this.props.onChange()
}
handleTagLabelClick(tag) { handleTagLabelClick(tag) {
const { dispatch } = this.props const { dispatch } = this.props

View File

@@ -20,7 +20,7 @@ const ToggleDirectionButton = ({ onClick, isRTL }) => (
ToggleDirectionButton.propTypes = { ToggleDirectionButton.propTypes = {
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
isRTL: PropTypes.string.isRequired isRTL: PropTypes.bool.isRequired
} }
export default CSSModules(ToggleDirectionButton, styles) export default CSSModules(ToggleDirectionButton, styles)

View File

@@ -16,8 +16,9 @@ import { store } from 'browser/main/store'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import { getLocales } from 'browser/lib/Languages' import { getLocales } from 'browser/lib/Languages'
import applyShortcuts from 'browser/main/lib/shortcutManager' import applyShortcuts from 'browser/main/lib/shortcutManager'
import uiThemes from 'browser/lib/ui-themes' import { chooseTheme, applyTheme } from 'browser/main/lib/ThemeManager'
import { push } from 'connected-react-router' import { push } from 'connected-react-router'
import { ipcRenderer } from 'electron'
const path = require('path') const path = require('path')
const electron = require('electron') const electron = require('electron')
@@ -148,11 +149,13 @@ class Main extends React.Component {
componentDidMount() { componentDidMount() {
const { dispatch, config } = this.props const { dispatch, config } = this.props
if (uiThemes.some(theme => theme.name === config.ui.theme)) { this.refreshTheme = setInterval(() => {
document.body.setAttribute('data-theme', config.ui.theme) const conf = ConfigManager.get()
} else { chooseTheme(conf)
document.body.setAttribute('data-theme', 'default') }, 5 * 1000)
}
chooseTheme(config)
applyTheme(config.ui.theme)
if (getLocales().indexOf(config.ui.language) !== -1) { if (getLocales().indexOf(config.ui.language) !== -1) {
i18n.setLocale(config.ui.language) i18n.setLocale(config.ui.language)
@@ -181,6 +184,8 @@ class Main extends React.Component {
'menubar:togglemenubar', 'menubar:togglemenubar',
this.toggleMenuBarVisible.bind(this) this.toggleMenuBarVisible.bind(this)
) )
eventEmitter.on('dispatch:push', this.changeRoutePush.bind(this))
eventEmitter.on('update', () => ipcRenderer.send('update-check', 'manual'))
} }
componentWillUnmount() { componentWillUnmount() {
@@ -189,6 +194,13 @@ class Main extends React.Component {
'menubar:togglemenubar', 'menubar:togglemenubar',
this.toggleMenuBarVisible.bind(this) this.toggleMenuBarVisible.bind(this)
) )
eventEmitter.off('dispatch:push', this.changeRoutePush.bind(this))
clearInterval(this.refreshTheme)
}
changeRoutePush(event, destination) {
const { dispatch } = this.props
dispatch(push(destination))
} }
toggleMenuBarVisible() { toggleMenuBarVisible() {

View File

@@ -21,6 +21,7 @@ import Markdown from '../../lib/markdown'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import context from 'browser/lib/context' import context from 'browser/lib/context'
import filenamify from 'filenamify'
import queryString from 'query-string' import queryString from 'query-string'
const { remote } = require('electron') const { remote } = require('electron')
@@ -634,6 +635,38 @@ class NoteList extends React.Component {
this.selectNextNote() this.selectNextNote()
} }
handleExportClick(e, note, fileType) {
const options = {
defaultPath: filenamify(note.title, {
replacement: '_'
}),
filters: [{ name: 'Documents', extensions: [fileType] }],
properties: ['openFile', 'createDirectory']
}
dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
if (filename) {
const { config } = this.props
dataApi
.exportNoteAs(note, filename, fileType, config)
.then(res => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: `Exported to ${filename}`
})
})
.catch(err => {
dialog.showErrorBox(
'Export error',
err ? err.message || err : 'Unexpected error during export'
)
throw err
})
}
})
}
handleNoteContextMenu(e, uniqueKey) { handleNoteContextMenu(e, uniqueKey) {
const { location } = this.props const { location } = this.props
const { selectedNoteKeys } = this.state const { selectedNoteKeys } = this.state
@@ -689,9 +722,40 @@ class NoteList extends React.Component {
click: this.copyNoteLink.bind(this, note) click: this.copyNoteLink.bind(this, note)
} }
) )
if (note.type === 'MARKDOWN_NOTE') { if (note.type === 'MARKDOWN_NOTE') {
templates.push(
{
type: 'separator'
},
{
label: i18n.__('Export Note'),
submenu: [
{
label: i18n.__('Export as Plain Text (.txt)'),
click: e => this.handleExportClick(e, note, 'txt')
},
{
label: i18n.__('Export as Markdown (.md)'),
click: e => this.handleExportClick(e, note, 'md')
},
{
label: i18n.__('Export as HTML (.html)'),
click: e => this.handleExportClick(e, note, 'html')
},
{
label: i18n.__('Export as PDF (.pdf)'),
click: e => this.handleExportClick(e, note, 'pdf')
}
]
}
)
if (note.blog && note.blog.blogLink && note.blog.blogId) { if (note.blog && note.blog.blogLink && note.blog.blogId) {
templates.push( templates.push(
{
type: 'separator'
},
{ {
label: updateLabel, label: updateLabel,
click: this.publishMarkdown.bind(this) click: this.publishMarkdown.bind(this)
@@ -702,10 +766,15 @@ class NoteList extends React.Component {
} }
) )
} else { } else {
templates.push({ templates.push(
label: publishLabel, {
click: this.publishMarkdown.bind(this) type: 'separator'
}) },
{
label: publishLabel,
click: this.publishMarkdown.bind(this)
}
)
} }
} }
} }
@@ -1107,10 +1176,10 @@ class NoteList extends React.Component {
getNoteFolder(note) { getNoteFolder(note) {
// note.folder = folder key // note.folder = folder key
return _.find( const storage = this.getNoteStorage(note)
this.getNoteStorage(note).folders, return storage
({ key }) => key === note.folder ? _.find(storage.folders, ({ key }) => key === note.folder)
) : []
} }
getViewType() { getViewType() {
@@ -1145,9 +1214,14 @@ class NoteList extends React.Component {
? this.getNotes().sort(sortFunc) ? this.getNotes().sort(sortFunc)
: this.sortByPin(this.getNotes().sort(sortFunc)) : this.sortByPin(this.getNotes().sort(sortFunc))
this.notes = notes = sortedNotes.filter(note => { this.notes = notes = sortedNotes.filter(note => {
// this is for the trash box if (
if (note.isTrashed !== true || location.pathname === '/trashed') // has matching storage
!!this.getNoteStorage(note) &&
// this is for the trash box
(note.isTrashed !== true || location.pathname === '/trashed')
) {
return true return true
}
}) })
if (sortDir === 'DESCENDING') this.notes.reverse() if (sortDir === 'DESCENDING') this.notes.reverse()
@@ -1193,6 +1267,8 @@ class NoteList extends React.Component {
sortBy === 'CREATED_AT' ? note.createdAt : note.updatedAt sortBy === 'CREATED_AT' ? note.createdAt : note.updatedAt
).fromNow('D') ).fromNow('D')
const storage = this.getNoteStorage(note)
if (isDefault) { if (isDefault) {
return ( return (
<NoteItem <NoteItem
@@ -1205,7 +1281,7 @@ class NoteList extends React.Component {
handleDragStart={this.handleDragStart.bind(this)} handleDragStart={this.handleDragStart.bind(this)}
pathname={location.pathname} pathname={location.pathname}
folderName={this.getNoteFolder(note).name} folderName={this.getNoteFolder(note).name}
storageName={this.getNoteStorage(note).name} storageName={storage.name}
viewType={viewType} viewType={viewType}
showTagsAlphabetically={config.ui.showTagsAlphabetically} showTagsAlphabetically={config.ui.showTagsAlphabetically}
coloredTags={config.coloredTags} coloredTags={config.coloredTags}
@@ -1223,7 +1299,7 @@ class NoteList extends React.Component {
handleDragStart={this.handleDragStart.bind(this)} handleDragStart={this.handleDragStart.bind(this)}
pathname={location.pathname} pathname={location.pathname}
folderName={this.getNoteFolder(note).name} folderName={this.getNoteFolder(note).name}
storageName={this.getNoteStorage(note).name} storageName={storage.name}
viewType={viewType} viewType={viewType}
/> />
) )

View File

@@ -43,12 +43,20 @@ class StorageItem extends React.Component {
label: i18n.__('Export Storage'), label: i18n.__('Export Storage'),
submenu: [ submenu: [
{ {
label: i18n.__('Export as txt'), label: i18n.__('Export as Plain Text (.txt)'),
click: e => this.handleExportStorageClick(e, 'txt') click: e => this.handleExportStorageClick(e, 'txt')
}, },
{ {
label: i18n.__('Export as md'), label: i18n.__('Export as Markdown (.md)'),
click: e => this.handleExportStorageClick(e, 'md') click: e => this.handleExportStorageClick(e, 'md')
},
{
label: i18n.__('Export as HTML (.html)'),
click: e => this.handleExportStorageClick(e, 'html')
},
{
label: i18n.__('Export as PDF (.pdf)'),
click: e => this.handleExportStorageClick(e, 'pdf')
} }
] ]
}, },
@@ -97,14 +105,28 @@ class StorageItem extends React.Component {
} }
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => { dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
if (paths && paths.length === 1) { if (paths && paths.length === 1) {
const { storage, dispatch } = this.props const { storage, dispatch, config } = this.props
dataApi.exportStorage(storage.key, fileType, paths[0]).then(data => { dataApi
dispatch({ .exportStorage(storage.key, fileType, paths[0], config)
type: 'EXPORT_STORAGE', .then(data => {
storage: data.storage, dialog.showMessageBox(remote.getCurrentWindow(), {
fileType: data.fileType type: 'info',
message: `Exported to ${paths[0]}`
})
dispatch({
type: 'EXPORT_STORAGE',
storage: data.storage,
fileType: data.fileType
})
})
.catch(error => {
dialog.showErrorBox(
'Export error',
error ? error.message || error : 'Unexpected error during export'
)
throw error
}) })
})
} }
}) })
} }
@@ -144,6 +166,15 @@ class StorageItem extends React.Component {
} }
} }
handleFolderMouseEnter(e, tooltipRef, isFolded) {
if (isFolded) {
const buttonEl = e.currentTarget
const tooltipEl = tooltipRef.current
tooltipEl.style.top = buttonEl.getBoundingClientRect().y + 'px'
}
}
handleFolderButtonContextMenu(e, folder) { handleFolderButtonContextMenu(e, folder) {
context.popup([ context.popup([
{ {
@@ -157,12 +188,20 @@ class StorageItem extends React.Component {
label: i18n.__('Export Folder'), label: i18n.__('Export Folder'),
submenu: [ submenu: [
{ {
label: i18n.__('Export as txt'), label: i18n.__('Export as Plain Text (.txt)'),
click: e => this.handleExportFolderClick(e, folder, 'txt') click: e => this.handleExportFolderClick(e, folder, 'txt')
}, },
{ {
label: i18n.__('Export as md'), label: i18n.__('Export as Markdown (.md)'),
click: e => this.handleExportFolderClick(e, folder, 'md') click: e => this.handleExportFolderClick(e, folder, 'md')
},
{
label: i18n.__('Export as HTML (.html)'),
click: e => this.handleExportFolderClick(e, folder, 'html')
},
{
label: i18n.__('Export as PDF (.pdf)'),
click: e => this.handleExportFolderClick(e, folder, 'pdf')
} }
] ]
}, },
@@ -193,30 +232,28 @@ class StorageItem extends React.Component {
} }
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => { dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
if (paths && paths.length === 1) { if (paths && paths.length === 1) {
const { storage, dispatch } = this.props const { storage, dispatch, config } = this.props
dataApi dataApi
.exportFolder(storage.key, folder.key, fileType, paths[0]) .exportFolder(storage.key, folder.key, fileType, paths[0], config)
.then(data => { .then(data => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: `Exported to ${paths[0]}`
})
dispatch({ dispatch({
type: 'EXPORT_FOLDER', type: 'EXPORT_FOLDER',
storage: data.storage, storage: data.storage,
folderKey: data.folderKey, folderKey: data.folderKey,
fileType: data.fileType fileType: data.fileType
}) })
return data
}) })
.then(data => { .catch(error => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: 'Exported to "' + data.exportDir + '"'
})
})
.catch(err => {
dialog.showErrorBox( dialog.showErrorBox(
'Export error', 'Export error',
err ? err.message || err : 'Unexpected error during export' error ? error.message || error : 'Unexpected error during export'
) )
throw err throw error
}) })
} }
}) })
@@ -316,6 +353,7 @@ class StorageItem extends React.Component {
folder.key folder.key
) )
const isActive = !!location.pathname.match(folderRegex) const isActive = !!location.pathname.match(folderRegex)
const tooltipRef = React.createRef(null)
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key) const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
let noteCount = 0 let noteCount = 0
@@ -339,7 +377,11 @@ class StorageItem extends React.Component {
key={folder.key} key={folder.key}
index={index} index={index}
isActive={isActive || folder.key === this.state.draggedOver} isActive={isActive || folder.key === this.state.draggedOver}
tooltipRef={tooltipRef}
handleButtonClick={e => this.handleFolderButtonClick(folder.key)(e)} handleButtonClick={e => this.handleFolderButtonClick(folder.key)(e)}
handleMouseEnter={e =>
this.handleFolderMouseEnter(e, tooltipRef, isFolded)
}
handleContextMenu={e => this.handleFolderButtonContextMenu(e, folder)} handleContextMenu={e => this.handleFolderButtonContextMenu(e, folder)}
folderName={folder.name} folderName={folder.name}
folderColor={folder.color} folderColor={folder.color}

View File

@@ -6,6 +6,7 @@ import dataApi from 'browser/main/lib/dataApi'
import styles from './SideNav.styl' import styles from './SideNav.styl'
import { openModal } from 'browser/main/lib/modal' import { openModal } from 'browser/main/lib/modal'
import PreferencesModal from '../modals/PreferencesModal' import PreferencesModal from '../modals/PreferencesModal'
import RenameTagModal from 'browser/main/modals/RenameTagModal'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import StorageItem from './StorageItem' import StorageItem from './StorageItem'
import TagListItem from 'browser/components/TagListItem' import TagListItem from 'browser/components/TagListItem'
@@ -25,6 +26,8 @@ import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import ColorPicker from 'browser/components/ColorPicker' import ColorPicker from 'browser/components/ColorPicker'
import { every, sortBy } from 'lodash' import { every, sortBy } from 'lodash'
const { dialog } = remote
function matchActiveTags(tags, activeTags) { function matchActiveTags(tags, activeTags) {
return every(activeTags, v => tags.indexOf(v) >= 0) return every(activeTags, v => tags.indexOf(v) >= 0)
} }
@@ -62,15 +65,12 @@ class SideNav extends React.Component {
} }
deleteTag(tag) { deleteTag(tag) {
const selectedButton = remote.dialog.showMessageBox( const selectedButton = dialog.showMessageBox(remote.getCurrentWindow(), {
remote.getCurrentWindow(), type: 'warning',
{ message: i18n.__('Confirm tag deletion'),
type: 'warning', detail: i18n.__('This will permanently remove this tag.'),
message: i18n.__('Confirm tag deletion'), buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
detail: i18n.__('This will permanently remove this tag.'), })
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}
)
if (selectedButton === 0) { if (selectedButton === 0) {
const { const {
@@ -154,23 +154,80 @@ class SideNav extends React.Component {
} }
handleTagContextMenu(e, tag) { handleTagContextMenu(e, tag) {
const menu = [] context.popup([
{
label: i18n.__('Rename Tag'),
click: this.handleRenameTagClick.bind(this, tag)
},
{
label: i18n.__('Customize Color'),
click: this.displayColorPicker.bind(
this,
tag,
e.target.getBoundingClientRect()
)
},
{
type: 'separator'
},
{
label: i18n.__('Export Tag'),
submenu: [
{
label: i18n.__('Export as Plain Text (.txt)'),
click: e => this.handleExportTagClick(e, tag, 'txt')
},
{
label: i18n.__('Export as Markdown (.md)'),
click: e => this.handleExportTagClick(e, tag, 'md')
},
{
label: i18n.__('Export as HTML (.html)'),
click: e => this.handleExportTagClick(e, tag, 'html')
},
{
label: i18n.__('Export as PDF (.pdf)'),
click: e => this.handleExportTagClick(e, tag, 'pdf')
}
]
},
{
type: 'separator'
},
{
label: i18n.__('Delete Tag'),
click: this.deleteTag.bind(this, tag)
}
])
}
menu.push({ handleExportTagClick(e, tag, fileType) {
label: i18n.__('Delete Tag'), const options = {
click: this.deleteTag.bind(this, tag) properties: ['openDirectory', 'createDirectory'],
buttonLabel: i18n.__('Select directory'),
title: i18n.__('Select a folder to export the files to'),
multiSelections: false
}
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
if (paths && paths.length === 1) {
const { data, config } = this.props
dataApi
.exportTag(data, tag, fileType, paths[0], config)
.then(data => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: `Exported to ${paths[0]}`
})
})
.catch(error => {
dialog.showErrorBox(
'Export error',
error ? error.message || error : 'Unexpected error during export'
)
throw error
})
}
}) })
menu.push({
label: i18n.__('Customize Color'),
click: this.displayColorPicker.bind(
this,
tag,
e.target.getBoundingClientRect()
)
})
context.popup(menu)
} }
dismissColorPicker() { dismissColorPicker() {
@@ -193,6 +250,16 @@ class SideNav extends React.Component {
}) })
} }
handleRenameTagClick(tagName) {
const { data, dispatch } = this.props
openModal(RenameTagModal, {
tagName,
data,
dispatch
})
}
handleColorPickerConfirm(color) { handleColorPickerConfirm(color) {
const { const {
dispatch, dispatch,
@@ -314,6 +381,7 @@ class SideNav extends React.Component {
dispatch={dispatch} dispatch={dispatch}
onSortEnd={this.onSortEnd.bind(this)(storage)} onSortEnd={this.onSortEnd.bind(this)(storage)}
useDragHandle useDragHandle
config={config}
/> />
) )
}) })

View File

@@ -12,6 +12,7 @@ import DevTools from './DevTools'
require('./lib/ipcClient') require('./lib/ipcClient')
require('../lib/customMeta') require('../lib/customMeta')
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import ConfigManager from './lib/ConfigManager'
const electron = require('electron') const electron = require('electron')
@@ -107,6 +108,22 @@ function updateApp() {
} }
} }
function downloadUpdate() {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Update Boostnote'),
detail: i18n.__('New Boostnote is ready to be downloaded.'),
buttons: [i18n.__('Download now'), i18n.__('Ignore updates')]
})
if (index === 0) {
ipcRenderer.send('update-download-confirm')
} else if (index === 1) {
ipcRenderer.send('update-cancel')
ConfigManager.set({ autoUpdateEnabled: false })
}
}
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
@@ -147,8 +164,12 @@ ReactDOM.render(
}) })
ipcRenderer.on('update-found', function() { ipcRenderer.on('update-found', function() {
notify('Update found!', { downloadUpdate()
body: 'Preparing to update...' })
ipcRenderer.on('update-not-found', function(_, msg) {
notify('Update not found!', {
body: msg
}) })
}) })

View File

@@ -2,7 +2,6 @@ import _ from 'lodash'
import RcParser from 'browser/lib/RcParser' import RcParser from 'browser/lib/RcParser'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import uiThemes from 'browser/lib/ui-themes'
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
const win = global.process.platform === 'win32' const win = global.process.platform === 'win32'
@@ -17,6 +16,22 @@ const DEFAULT_MARKDOWN_LINT_CONFIG = `{
"default": true "default": true
}` }`
const DEFAULT_CSS_CONFIG = `
/* Drop Your Custom CSS Code Here */
[data-theme="default"] p code.inline,
[data-theme="default"] li code.inline,
[data-theme="default"] td code.inline
{
padding: 2px;
border-width: 1px;
border-style: solid;
border-radius: 5px;
background-color: #F4F4F4;
border-color: #d9d9d9;
color: #03C588;
}
`
export const DEFAULT_CONFIG = { export const DEFAULT_CONFIG = {
zoom: 1, zoom: 1,
isSideNavFolded: false, isSideNavFolded: false,
@@ -33,7 +48,7 @@ export const DEFAULT_CONFIG = {
hotkey: { hotkey: {
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E', toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M', toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
toggleDirection: OSX ? 'Command + Alt + Right' : 'Ctrl + Right', toggleDirection: OSX ? 'Command + Alt + Right' : 'Ctrl + Alt + Right',
deleteNote: OSX deleteNote: OSX
? 'Command + Shift + Backspace' ? 'Command + Shift + Backspace'
: 'Ctrl + Shift + Backspace', : 'Ctrl + Shift + Backspace',
@@ -47,11 +62,17 @@ export const DEFAULT_CONFIG = {
ui: { ui: {
language: 'en', language: 'en',
theme: 'default', theme: 'default',
defaultTheme: 'default',
enableScheduleTheme: false,
scheduledTheme: 'monokai',
scheduleStart: 1200,
scheduleEnd: 360,
showCopyNotification: true, showCopyNotification: true,
disableDirectWrite: false, disableDirectWrite: false,
showScrollBar: true, showScrollBar: true,
defaultNote: 'ALWAYS_ASK', // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE' defaultNote: 'ALWAYS_ASK', // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
showMenuBar: false showMenuBar: false,
isStacking: false
}, },
editor: { editor: {
theme: 'base16-light', theme: 'base16-light',
@@ -65,8 +86,13 @@ export const DEFAULT_CONFIG = {
rulers: [80, 120], rulers: [80, 120],
displayLineNumbers: true, displayLineNumbers: true,
matchingPairs: '()[]{}\'\'""$$**``~~__', matchingPairs: '()[]{}\'\'""$$**``~~__',
matchingCloseBefore: ')]}\'":;>',
matchingTriples: '```"""\'\'\'', matchingTriples: '```"""\'\'\'',
explodingPairs: '[]{}``$$', explodingPairs: '[]{}``$$',
codeBlockMatchingPairs: '()[]{}\'\'""``',
codeBlockMatchingCloseBefore: ')]}\'":;>',
codeBlockMatchingTriples: '',
codeBlockExplodingPairs: '[]{}``',
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK' switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE' delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE'
scrollPastEnd: false, scrollPastEnd: false,
@@ -79,13 +105,15 @@ export const DEFAULT_CONFIG = {
enableSmartPaste: false, enableSmartPaste: false,
enableMarkdownLint: false, enableMarkdownLint: false,
customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG, customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG,
prettierConfig: ` { dateFormatISO8601: false,
prettierConfig: `{
"trailingComma": "es5", "trailingComma": "es5",
"tabWidth": 2, "tabWidth": 2,
"semi": false, "semi": false,
"singleQuote": true "singleQuote": true
}`, }`,
deleteUnusedAttachments: true deleteUnusedAttachments: true,
rtlEnabled: false
}, },
preview: { preview: {
fontSize: '14', fontSize: '14',
@@ -103,8 +131,7 @@ export const DEFAULT_CONFIG = {
breaks: true, breaks: true,
smartArrows: false, smartArrows: false,
allowCustomCSS: false, allowCustomCSS: false,
customCSS: DEFAULT_CSS_CONFIG,
customCSS: '/* Drop Your Custom CSS Code Here */',
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE' sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
mermaidHTMLLabel: false, mermaidHTMLLabel: false,
lineThroughCheckbox: true lineThroughCheckbox: true
@@ -117,7 +144,15 @@ export const DEFAULT_CONFIG = {
username: '', username: '',
password: '' password: ''
}, },
coloredTags: {} export: {
metadata: 'DONT_EXPORT', // 'DONT_EXPORT', 'MERGE_HEADER', 'MERGE_VARIABLE'
variable: 'boostnote',
prefixAttachmentFolder: false
},
coloredTags: {},
wakatime: {
key: null
}
} }
function validate(config) { function validate(config) {
@@ -199,12 +234,6 @@ function set(updates) {
if (!validate(newConfig)) throw new Error('INVALID CONFIG') if (!validate(newConfig)) throw new Error('INVALID CONFIG')
_save(newConfig) _save(newConfig)
if (uiThemes.some(theme => theme.name === newConfig.ui.theme)) {
document.body.setAttribute('data-theme', newConfig.ui.theme)
} else {
document.body.setAttribute('data-theme', 'default')
}
i18n.setLocale(newConfig.ui.language) i18n.setLocale(newConfig.ui.language)
let editorTheme = document.getElementById('editorTheme') let editorTheme = document.getElementById('editorTheme')
@@ -239,6 +268,12 @@ function assignConfigValues(originalConfig, rcConfig) {
originalConfig.hotkey, originalConfig.hotkey,
rcConfig.hotkey rcConfig.hotkey
) )
config.wakatime = Object.assign(
{},
DEFAULT_CONFIG.wakatime,
originalConfig.wakatime,
rcConfig.wakatime
)
config.blog = Object.assign( config.blog = Object.assign(
{}, {},
DEFAULT_CONFIG.blog, DEFAULT_CONFIG.blog,

View File

@@ -0,0 +1,59 @@
import ConfigManager from 'browser/main/lib/ConfigManager'
import uiThemes from 'browser/lib/ui-themes'
const saveChanges = newConfig => {
ConfigManager.set(newConfig)
}
const chooseTheme = config => {
const { ui } = config
if (!ui.enableScheduleTheme) {
return
}
const start = parseInt(ui.scheduleStart)
const end = parseInt(ui.scheduleEnd)
const now = new Date()
const minutes = now.getHours() * 60 + now.getMinutes()
const isEndAfterStart = end > start
const isBetweenStartAndEnd = minutes >= start && minutes < end
const isBetweenEndAndStart = minutes >= start || minutes < end
if (
(isEndAfterStart && isBetweenStartAndEnd) ||
(!isEndAfterStart && isBetweenEndAndStart)
) {
if (ui.theme !== ui.scheduledTheme) {
ui.defaultTheme = ui.theme
ui.theme = ui.scheduledTheme
applyTheme(ui.theme)
saveChanges(config)
}
} else {
if (ui.theme !== ui.defaultTheme) {
ui.theme = ui.defaultTheme
applyTheme(ui.theme)
saveChanges(config)
}
}
}
const applyTheme = theme => {
if (uiThemes.some(item => item.name === theme)) {
document.body.setAttribute('data-theme', theme)
if (document.body.querySelector('.MarkdownPreview')) {
document.body
.querySelector('.MarkdownPreview')
.contentDocument.body.setAttribute('data-theme', theme)
}
} else {
document.body.setAttribute('data-theme', 'default')
}
}
module.exports = {
chooseTheme,
applyTheme
}

View File

@@ -706,31 +706,38 @@ function replaceNoteKeyWithNewNoteKey(noteContent, oldNoteKey, newNoteKey) {
} }
/** /**
* @description Deletes all :storage and noteKey references from the given input. * @description replace all :storage references with given destination folder.
* @param input Input in which the references should be deleted * @param input Input in which the references should be replaced
* @param noteKey Key of the current note * @param noteKey Key of the current note
* @param destinationFolder Destination folder of the attachements
* @returns {String} Input without the references * @returns {String} Input without the references
*/ */
function removeStorageAndNoteReferences(input, noteKey) { function replaceStorageReferences(input, noteKey, destinationFolder) {
return input.replace( return input.replace(
new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?("|])', 'g'), new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '[^"\\)<\\s]+', 'g'),
function(match) { function(match) {
const temp = match return match
.replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.sep) .replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.posix.sep)
.replace(new RegExp(mdurl.encode(path.posix.sep), 'g'), path.sep) .replace(new RegExp(mdurl.encode(path.posix.sep), 'g'), path.posix.sep)
.replace(new RegExp(escapeStringRegexp(path.win32.sep), 'g'), path.sep) .replace(
.replace(new RegExp(escapeStringRegexp(path.posix.sep), 'g'), path.sep) new RegExp(escapeStringRegexp(path.win32.sep), 'g'),
return temp.replace( path.posix.sep
new RegExp( )
STORAGE_FOLDER_PLACEHOLDER + .replace(
'(' + new RegExp(escapeStringRegexp(path.posix.sep), 'g'),
escapeStringRegexp(path.sep) + path.posix.sep
noteKey + )
')?', .replace(
'g' new RegExp(
), STORAGE_FOLDER_PLACEHOLDER +
DESTINATION_FOLDER '(' +
) escapeStringRegexp(path.sep) +
noteKey +
')?',
'g'
),
destinationFolder
)
} }
) )
} }
@@ -835,7 +842,15 @@ function getAttachmentsPathAndStatus(markdownContent, storageKey, noteKey) {
if (storageKey == null || noteKey == null || markdownContent == null) { if (storageKey == null || noteKey == null || markdownContent == null) {
return null return null
} }
const targetStorage = findStorage.findStorage(storageKey) let targetStorage = null
try {
targetStorage = findStorage.findStorage(storageKey)
} catch (error) {
console.warn(`No stroage found for: ${storageKey}`)
}
if (!targetStorage) {
return null
}
const attachmentFolder = path.join( const attachmentFolder = path.join(
targetStorage.path, targetStorage.path,
DESTINATION_FOLDER, DESTINATION_FOLDER,
@@ -1087,8 +1102,8 @@ module.exports = {
getAttachmentsInMarkdownContent, getAttachmentsInMarkdownContent,
getAbsolutePathsOfAttachmentsInContent, getAbsolutePathsOfAttachmentsInContent,
importAttachments, importAttachments,
removeStorageAndNoteReferences,
removeAttachmentsByPaths, removeAttachmentsByPaths,
replaceStorageReferences,
deleteAttachmentFolder, deleteAttachmentFolder,
deleteAttachmentsNotPresentInNote, deleteAttachmentsNotPresentInNote,
getAttachmentsPathAndStatus, getAttachmentsPathAndStatus,

View File

@@ -1,5 +1,6 @@
const fs = require('fs') import fs from 'fs'
const path = require('path') import fx from 'fs-extra'
import path from 'path'
/** /**
* @description Copy a file from source to destination * @description Copy a file from source to destination
@@ -14,7 +15,8 @@ function copyFile(srcPath, dstPath) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const dstFolder = path.dirname(dstPath) const dstFolder = path.dirname(dstPath)
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
fx.ensureDirSync(dstFolder)
const input = fs.createReadStream(decodeURI(srcPath)) const input = fs.createReadStream(decodeURI(srcPath))
const output = fs.createWriteStream(dstPath) const output = fs.createWriteStream(dstPath)

View File

@@ -1,15 +1,16 @@
import { findStorage } from 'browser/lib/findStorage' import { findStorage } from 'browser/lib/findStorage'
import resolveStorageData from './resolveStorageData' import resolveStorageData from './resolveStorageData'
import resolveStorageNotes from './resolveStorageNotes' import resolveStorageNotes from './resolveStorageNotes'
import getFilename from './getFilename'
import exportNote from './exportNote' import exportNote from './exportNote'
import filenamify from 'filenamify' import getContentFormatter from './getContentFormatter'
import * as path from 'path'
/** /**
* @param {String} storageKey * @param {String} storageKey
* @param {String} folderKey * @param {String} folderKey
* @param {String} fileType * @param {String} fileType
* @param {String} exportDir * @param {String} exportDir
* @param {Object} config
* *
* @return {Object} * @return {Object}
* ``` * ```
@@ -22,7 +23,7 @@ import * as path from 'path'
* ``` * ```
*/ */
function exportFolder(storageKey, folderKey, fileType, exportDir) { function exportFolder(storageKey, folderKey, fileType, exportDir, config) {
let targetStorage let targetStorage
try { try {
targetStorage = findStorage(storageKey) targetStorage = findStorage(storageKey)
@@ -30,39 +31,34 @@ function exportFolder(storageKey, folderKey, fileType, exportDir) {
return Promise.reject(e) return Promise.reject(e)
} }
const deduplicator = {}
return resolveStorageData(targetStorage) return resolveStorageData(targetStorage)
.then(function assignNotes(storage) { .then(storage => {
return resolveStorageNotes(storage).then(notes => { return resolveStorageNotes(storage).then(notes => ({
return { storage,
storage, notes: notes.filter(
notes note =>
} note.folder === folderKey &&
}) !note.isTrashed &&
note.type === 'MARKDOWN_NOTE'
)
}))
}) })
.then(function exportNotes(data) { .then(({ storage, notes }) => {
const { storage, notes } = data const contentFormatter = getContentFormatter(storage, fileType, config)
return Promise.all( return Promise.all(
notes notes.map(note => {
.filter( const targetPath = getFilename(
note => note,
note.folder === folderKey && fileType,
note.isTrashed === false && exportDir,
note.type === 'MARKDOWN_NOTE' deduplicator
) )
.map(note => {
const notePath = path.join( return exportNote(storage.key, note, targetPath, contentFormatter)
exportDir, })
`${filenamify(note.title, { replacement: '_' })}.${fileType}`
)
return exportNote(
note.key,
storage.path,
note.content,
notePath,
null
)
})
).then(() => ({ ).then(() => ({
storage, storage,
folderKey, folderKey,

View File

@@ -4,58 +4,35 @@ import { findStorage } from 'browser/lib/findStorage'
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const attachmentManagement = require('./attachmentManagement')
/** /**
* Export note together with attachments * Export note together with attachments
* *
* If attachments are stored in the storage, creates 'attachments' subfolder in target directory * If attachments are stored in the storage, creates 'attachments' subfolder in target directory
* and copies attachments to it. Changes links to images in the content of the note * and copies attachments to it. Changes links to images in the content of the note
* *
* @param {String} nodeKey key of the node that should be exported
* @param {String} storageKey or storage path * @param {String} storageKey or storage path
* @param {String} noteContent Content to export * @param {Object} note Note to export
* @param {String} targetPath Path to exported file * @param {String} targetPath Path to exported file
* @param {function} outputFormatter * @param {function} outputFormatter
* @return {Promise.<*[]>} * @return {Promise.<*[]>}
*/ */
function exportNote( function exportNote(storageKey, note, targetPath, outputFormatter) {
nodeKey,
storageKey,
noteContent,
targetPath,
outputFormatter
) {
const storagePath = path.isAbsolute(storageKey) const storagePath = path.isAbsolute(storageKey)
? storageKey ? storageKey
: findStorage(storageKey).path : findStorage(storageKey).path
const exportTasks = [] const exportTasks = []
if (!storagePath) { if (!storagePath) {
throw new Error('Storage path is not found') throw new Error('Storage path is not found')
} }
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
storagePath
)
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER
})
})
let exportedData = attachmentManagement.removeStorageAndNoteReferences( const exportedData = Promise.resolve(
noteContent, outputFormatter
nodeKey ? outputFormatter(note, targetPath, exportTasks)
: note.content
) )
if (outputFormatter) {
exportedData = outputFormatter(exportedData, exportTasks, targetPath)
} else {
exportedData = Promise.resolve(exportedData)
}
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath)) const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
return Promise.all(tasks.map(task => copyFile(task.src, task.dst))) return Promise.all(tasks.map(task => copyFile(task.src, task.dst)))
@@ -63,9 +40,9 @@ function exportNote(
.then(data => { .then(data => {
return saveToFile(data, targetPath) return saveToFile(data, targetPath)
}) })
.catch(err => { .catch(error => {
rollbackExport(tasks) rollbackExport(tasks)
throw err throw error
}) })
} }
@@ -107,14 +84,14 @@ function rollbackExport(tasks) {
} }
if (fs.existsSync(fullpath)) { if (fs.existsSync(fullpath)) {
fs.unlink(fullpath) fs.unlinkSync(fullpath)
folders.add(path.dirname(fullpath)) folders.add(path.dirname(fullpath))
} }
}) })
folders.forEach(folder => { folders.forEach(folder => {
if (fs.readdirSync(folder).length === 0) { if (fs.readdirSync(folder).length === 0) {
fs.rmdir(folder) fs.rmdirSync(folder)
} }
}) })
} }

View File

@@ -0,0 +1,19 @@
import { findStorage } from 'browser/lib/findStorage'
import exportNote from './exportNote'
import getContentFormatter from './getContentFormatter'
/**
* @param {Object} note
* @param {String} filename
* @param {String} fileType
* @param {Object} config
*/
function exportNoteAs(note, filename, fileType, config) {
const storage = findStorage(note.storage)
const contentFormatter = getContentFormatter(storage, fileType, config)
return exportNote(storage.key, note, filename, contentFormatter)
}
module.exports = exportNoteAs

View File

@@ -2,13 +2,17 @@ import { findStorage } from 'browser/lib/findStorage'
import resolveStorageData from './resolveStorageData' import resolveStorageData from './resolveStorageData'
import resolveStorageNotes from './resolveStorageNotes' import resolveStorageNotes from './resolveStorageNotes'
import filenamify from 'filenamify' import filenamify from 'filenamify'
import * as path from 'path' import path from 'path'
import * as fs from 'fs' import fs from 'fs'
import exportNote from './exportNote'
import getContentFormatter from './getContentFormatter'
import getFilename from './getFilename'
/** /**
* @param {String} storageKey * @param {String} storageKey
* @param {String} fileType * @param {String} fileType
* @param {String} exportDir * @param {String} exportDir
* @param {Object} config
* *
* @return {Object} * @return {Object}
* ``` * ```
@@ -20,7 +24,7 @@ import * as fs from 'fs'
* ``` * ```
*/ */
function exportStorage(storageKey, fileType, exportDir) { function exportStorage(storageKey, fileType, exportDir, config) {
let targetStorage let targetStorage
try { try {
targetStorage = findStorage(storageKey) targetStorage = findStorage(storageKey)
@@ -29,39 +33,52 @@ function exportStorage(storageKey, fileType, exportDir) {
} }
return resolveStorageData(targetStorage) return resolveStorageData(targetStorage)
.then(storage => .then(storage => {
resolveStorageNotes(storage).then(notes => ({ storage, notes })) return resolveStorageNotes(storage).then(notes => ({
) storage,
.then(function exportNotes(data) { notes: notes.filter(
const { storage, notes } = data note => !note.isTrashed && note.type === 'MARKDOWN_NOTE'
)
}))
})
.then(({ storage, notes }) => {
const contentFormatter = getContentFormatter(storage, fileType, config)
const folderNamesMapping = {} const folderNamesMapping = {}
const deduplicators = {}
storage.folders.forEach(folder => { storage.folders.forEach(folder => {
const folderExportedDir = path.join( const folderExportedDir = path.join(
exportDir, exportDir,
filenamify(folder.name, { replacement: '_' }) filenamify(folder.name, { replacement: '_' })
) )
folderNamesMapping[folder.key] = folderExportedDir folderNamesMapping[folder.key] = folderExportedDir
// make sure directory exists // make sure directory exists
try { try {
fs.mkdirSync(folderExportedDir) fs.mkdirSync(folderExportedDir)
} catch (e) {} } catch (e) {}
})
notes
.filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
.forEach(markdownNote => {
const folderExportedDir = folderNamesMapping[markdownNote.folder]
const snippetName = `${filenamify(markdownNote.title, {
replacement: '_'
})}.${fileType}`
const notePath = path.join(folderExportedDir, snippetName)
fs.writeFileSync(notePath, markdownNote.content)
})
return { deduplicators[folder.key] = {}
})
return Promise.all(
notes.map(note => {
const targetPath = getFilename(
note,
fileType,
folderNamesMapping[note.folder],
deduplicators[note.folder]
)
return exportNote(storage.key, note, targetPath, contentFormatter)
})
).then(() => ({
storage, storage,
fileType, fileType,
exportDir exportDir
} }))
}) })
} }

View File

@@ -0,0 +1,28 @@
import exportNoteAs from './exportNoteAs'
import getFilename from './getFilename'
/**
* @param {Object} data
* @param {String} tag
* @param {String} fileType
* @param {String} exportDir
* @param {Object} config
*/
function exportTag(data, tag, fileType, exportDir, config) {
const notes = data.noteMap
.map(note => note)
.filter(note => note.tags.indexOf(tag) !== -1)
const deduplicator = {}
return Promise.all(
notes.map(note => {
const filename = getFilename(note, fileType, exportDir, deduplicator)
return exportNoteAs(note, filename, fileType, config)
})
)
}
module.exports = exportTag

View File

@@ -0,0 +1,796 @@
import path from 'path'
import fileUrl from 'file-url'
import fs from 'fs'
import { remote } from 'electron'
import consts from 'browser/lib/consts'
import Markdown from 'browser/lib/markdown'
import attachmentManagement from './attachmentManagement'
import { version as codemirrorVersion } from 'codemirror/package.json'
import { escapeHtmlCharacters } from 'browser/lib/utils'
const { app } = remote
const appPath = fileUrl(
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
)
let markdownStyle = ''
try {
markdownStyle = require('!!css!stylus?sourceMap!../../../components/markdown.styl')[0][1]
} catch (e) {}
export const CSS_FILES = [
`${appPath}/node_modules/katex/dist/katex.min.css`,
`${appPath}/node_modules/codemirror/lib/codemirror.css`,
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
]
const macos = global.process.platform === 'darwin'
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
if (!macos) {
defaultFontFamily.unshift('Microsoft YaHei')
defaultFontFamily.unshift('meiryo')
}
const defaultCodeBlockFontFamily = [
'Monaco',
'Menlo',
'Ubuntu Mono',
'Consolas',
'source-code-pro',
'monospace'
]
function unprefix(file) {
if (global.process.platform === 'win32') {
return file.replace('file:///', '')
} else {
return file.replace('file://', '')
}
}
/**
* ```
* {
* fontFamily,
* fontSize,
* lineNumber,
* codeBlockFontFamily,
* codeBlockTheme,
* scrollPastEnd,
* theme,
* allowCustomCSS,
* customCSS
* smartQuotes,
* sanitize,
* breaks,
* storagePath,
* export,
* indentSize
* }
* ```
*/
export default function formatHTML(props) {
const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = getStyleParams(props)
const inlineStyles = buildStyle(
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
)
const { smartQuotes, sanitize, breaks } = props
let indentSize = parseInt(props.indentSize, 10)
if (!(indentSize > 0 && indentSize < 132)) {
indentSize = 4
}
const files = [getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
return function(note, targetPath, exportTasks) {
const styles = files
.map(file => `<link rel="stylesheet" href="css/${path.basename(file)}">`)
.join('\n')
let inlineScripts = ''
let scripts = ''
let decodeEntities = false
function addDecodeEntities() {
if (decodeEntities) {
return
}
decodeEntities = true
inlineScripts += `
function decodeEntities (text) {
var entities = [
['apos', '\\''],
['amp', '&'],
['lt', '<'],
['gt', '>'],
['#63', '\\?'],
['#36', '\\$']
]
for (var i = 0, max = entities.length; i < max; ++i) {
text = text.replace(new RegExp(\`&\${entities[i][0]};\`, 'g'), entities[i][1])
}
return text
}`
}
let lodash = false
function addLodash() {
if (lodash) {
return
}
lodash = true
exportTasks.push({
src: unprefix(`${appPath}/node_modules/lodash/lodash.min.js`),
dst: 'js'
})
scripts += `<script src="js/lodash.min.js"></script>`
}
let raphael = false
function addRaphael() {
if (raphael) {
return
}
raphael = true
exportTasks.push({
src: unprefix(`${appPath}/node_modules/raphael/raphael.min.js`),
dst: 'js'
})
scripts += `<script src="js/raphael.min.js"></script>`
}
let yaml = false
function addYAML() {
if (yaml) {
return
}
yaml = true
exportTasks.push({
src: unprefix(`${appPath}/node_modules/js-yaml/dist/js-yaml.min.js`),
dst: 'js'
})
scripts += `<script src="js/js-yaml.min.js"></script>`
}
let chart = false
function addChart() {
if (chart) {
return
}
chart = true
addLodash()
exportTasks.push({
src: unprefix(`${appPath}/node_modules/chart.js/dist/Chart.min.js`),
dst: 'js'
})
scripts += `<script src="js/Chart.min.js"></script>`
inlineScripts += `
function displayCharts() {
_.forEach(
document.querySelectorAll('.chart'),
el => {
try {
const format = el.attributes.getNamedItem('data-format').value
const chartConfig = format === 'yaml' ? jsyaml.load(el.innerHTML) : JSON.parse(el.innerHTML)
el.innerHTML = ''
const canvas = document.createElement('canvas')
el.appendChild(canvas)
const height = el.attributes.getNamedItem('data-height')
if (height && height.value !== 'undefined') {
el.style.height = height.value + 'vh'
canvas.height = height.value + 'vh'
}
const chart = new Chart(canvas, chartConfig)
} catch (e) {
el.className = 'chart-error'
el.innerHTML = 'chartjs diagram parse error: ' + e.message
}
}
)
}
document.addEventListener('DOMContentLoaded', displayCharts);
`
}
let codemirror = false
function addCodeMirror() {
if (codemirror) {
return
}
codemirror = true
addDecodeEntities()
addLodash()
exportTasks.push(
{
src: unprefix(`${appPath}/node_modules/codemirror/lib/codemirror.js`),
dst: 'js/codemirror'
},
{
src: unprefix(`${appPath}/node_modules/codemirror/mode/meta.js`),
dst: 'js/codemirror/mode'
},
{
src: unprefix(
`${appPath}/node_modules/codemirror/addon/mode/loadmode.js`
),
dst: 'js/codemirror/addon/mode'
},
{
src: unprefix(
`${appPath}/node_modules/codemirror/addon/runmode/runmode.js`
),
dst: 'js/codemirror/addon/runmode'
}
)
scripts += `
<script src="js/codemirror/codemirror.js"></script>
<script src="js/codemirror/mode/meta.js"></script>
<script src="js/codemirror/addon/mode/loadmode.js"></script>
<script src="js/codemirror/addon/runmode/runmode.js"></script>
`
let className = `cm-s-${codeBlockTheme}`
if (codeBlockTheme.indexOf('solarized') === 0) {
const [refThema, color] = codeBlockTheme.split(' ')
className = `cm-s-${refThema} cm-s-${color}`
}
inlineScripts += `
CodeMirror.modeURL = 'https://cdn.jsdelivr.net/npm/codemirror@${codemirrorVersion}/mode/%N/%N.js';
function displayCodeBlocks() {
_.forEach(
document.querySelectorAll('.code code'),
el => {
el.parentNode.className += ' ${className}'
let syntax = CodeMirror.findModeByName(el.className)
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
CodeMirror.requireMode(syntax.mode, () => {
const content = decodeEntities(el.innerHTML)
el.innerHTML = ''
CodeMirror.runMode(content, syntax.mime, el, {
tabSize: ${indentSize}
})
})
}
)
}
document.addEventListener('DOMContentLoaded', displayCodeBlocks);
`
}
let flowchart = false
function addFlowchart() {
if (flowchart) {
return
}
flowchart = true
addDecodeEntities()
addLodash()
addRaphael()
exportTasks.push({
src: unprefix(
`${appPath}/node_modules/flowchart.js/release/flowchart.min.js`
),
dst: 'js'
})
scripts += `<script src="js/flowchart.min.js"></script>`
inlineScripts += `
function displayFlowcharts() {
_.forEach(
document.querySelectorAll('.flowchart'),
el => {
try {
const diagram = flowchart.parse(
decodeEntities(el.innerHTML)
)
el.innerHTML = ''
diagram.drawSVG(el)
} catch (e) {
el.className = 'flowchart-error'
el.innerHTML = 'Flowchart parse error: ' + e.message
}
}
)
}
document.addEventListener('DOMContentLoaded', displayFlowcharts);
`
}
let mermaid = false
function addMermaid() {
if (mermaid) {
return
}
mermaid = true
addLodash()
exportTasks.push({
src: unprefix(`${appPath}/node_modules/mermaid/dist/mermaid.min.js`),
dst: 'js'
})
scripts += `<script src="js/mermaid.min.js"></script>`
inlineScripts += `
function displayMermaids() {
_.forEach(
document.querySelectorAll('.mermaid'),
el => {
const height = el.attributes.getNamedItem('data-height')
if (height && height.value !== 'undefined') {
el.style.height = height.value + 'vh'
}
}
)
}
document.addEventListener('DOMContentLoaded', displayMermaids);
`
}
let sequence = false
function addSequence() {
if (sequence) {
return
}
sequence = true
addDecodeEntities()
addLodash()
addRaphael()
exportTasks.push({
src: unprefix(
`${appPath}/node_modules/@rokt33r/js-sequence-diagrams/dist/sequence-diagram-min.js`
),
dst: 'js'
})
scripts += `<script src="js/sequence-diagram-min.js"></script>`
inlineScripts += `
function displaySequences() {
_.forEach(
document.querySelectorAll('.sequence'),
el => {
try {
const diagram = Diagram.parse(
decodeEntities(el.innerHTML)
)
el.innerHTML = ''
diagram.drawSVG(el, { theme: 'simple' })
} catch (e) {
el.className = 'sequence-error'
el.innerHTML = 'Sequence diagram parse error: ' + e.message
}
}
)
}
document.addEventListener('DOMContentLoaded', displaySequences);
`
}
const modes = {}
const markdown = new Markdown({
typographer: smartQuotes,
sanitize,
breaks,
onFence(type, mode) {
if (type === 'chart') {
addChart()
if (mode === 'yaml') {
addYAML()
}
} else if (type === 'code') {
addCodeMirror()
if (mode && modes[mode] !== true) {
const file = unprefix(
`${appPath}/node_modules/codemirror/mode/${mode}/${mode}.js`
)
if (fs.existsSync(file)) {
exportTasks.push({
src: file,
dst: `js/codemirror/mode/${mode}`
})
modes[mode] = true
}
}
} else if (type === 'flowchart') {
addFlowchart()
} else if (type === 'mermaid') {
addMermaid()
} else if (type === 'sequence') {
addSequence()
}
}
})
let body = note.content
if (sanitize === 'NONE') {
body = escapeHtmlCharactersInCodeTag(body.split('```'))
}
body = markdown.render(note.content)
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
note.content,
props.storagePath
)
files.forEach(file => {
exportTasks.push({
src: unprefix(file),
dst: 'css'
})
})
const destinationFolder = props.export.prefixAttachmentFolder
? `${path.parse(targetPath).name} - ${
attachmentManagement.DESTINATION_FOLDER
}`
: attachmentManagement.DESTINATION_FOLDER
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: destinationFolder
})
})
body = attachmentManagement.replaceStorageReferences(
body,
note.key,
destinationFolder
)
return `
<html>
<head>
<meta charset="UTF-8">
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
<style id="style">${inlineStyles}</style>
${styles}
${scripts}
<script>${inlineScripts}</script>
</head>
<body data-theme="${theme}">
${body}
</body>
</html>
`
}
}
export function getStyleParams(props) {
const {
fontSize,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS,
RTL
} = props
let { fontFamily, codeBlockFontFamily } = props
fontFamily =
_.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultFontFamily)
: defaultFontFamily
codeBlockFontFamily =
_.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
? codeBlockFontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
return {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS,
RTL
}
}
export function getCodeThemeLink(name) {
const theme = consts.THEMES.find(theme => theme.name === name)
return theme != null
? theme.path
: `${appPath}/node_modules/codemirror/theme/elegant.css`
}
export function buildStyle(
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS,
RTL
) {
return `
@font-face {
font-family: 'Lato';
src: url('${appPath}/resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
}
@font-face {
font-family: 'Lato';
src: url('${appPath}/resources/fonts/Lato-Black.woff2') format('woff2'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Black.woff') format('woff'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Black.ttf') format('truetype');
font-style: normal;
font-weight: 700;
text-rendering: optimizeLegibility;
}
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff2') format('woff2'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
}
${markdownStyle}
body {
font-family: '${fontFamily.join("','")}';
font-size: ${fontSize}px;
${scrollPastEnd && 'padding-bottom: 90vh;box-sizing: border-box;'}
${RTL && 'direction: rtl;text-align: right;'}
}
@media print {
body {
padding-bottom: initial;
}
}
code {
font-family: '${codeBlockFontFamily.join("','")}';
background-color: rgba(0,0,0,0.04);
text-align: left;
direction: ltr;
}
p code.inline,
li code.inline,
td code.inline
{
padding: 2px;
border-width: 1px;
border-style: solid;
border-radius: 5px;
}
[data-theme="default"] p code.inline,
[data-theme="default"] li code.inline,
[data-theme="default"] td code.inline
{
background-color: #F4F4F4;
border-color: #d9d9d9;
color: inherit;
}
[data-theme="white"] p code.inline,
[data-theme="white"] li code.inline,
[data-theme="white"] td code.inline
{
background-color: #F4F4F4;
border-color: #d9d9d9;
color: inherit;
}
[data-theme="dark"] p code.inline,
[data-theme="dark"] li code.inline,
[data-theme="dark"] td code.inline
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="dracula"] p code.inline,
[data-theme="dracula"] li code.inline,
[data-theme="dracula"] td code.inline
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="monokai"] p code.inline,
[data-theme="monokai"] li code.inline,
[data-theme="monokai"] td code.inline
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="nord"] p code.inline,
[data-theme="nord"] li code.inline,
[data-theme="nord"] td code.inline
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="solarized-dark"] p code.inline,
[data-theme="solarized-dark"] li code.inline,
[data-theme="solarized-dark"] td code.inline
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="vulcan"] p code.inline,
[data-theme="vulcan"] li code.inline,
[data-theme="vulcan"] td code.inline
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
.lineNumber {
${lineNumber && 'display: block !important;'}
font-family: '${codeBlockFontFamily.join("','")}';
}
.clipboardButton {
color: rgba(147,147,149,0.8);;
fill: rgba(147,147,149,1);;
border-radius: 50%;
margin: 0px 10px;
border: none;
background-color: transparent;
outline: none;
height: 15px;
width: 15px;
cursor: pointer;
}
.clipboardButton:hover {
transition: 0.2s;
color: #939395;
fill: #939395;
background-color: rgba(0,0,0,0.1);
}
h1, h2 {
border: none;
}
h1 {
padding-bottom: 4px;
margin: 1em 0 8px;
}
h2 {
padding-bottom: 0.2em;
margin: 1em 0 0.37em;
}
body p {
white-space: normal;
}
@media print {
body[data-theme="${theme}"] {
color: #000;
background-color: #fff;
}
.clipboardButton {
display: none
}
}
${allowCustomCSS ? customCSS : ''}
`
}
/**
* @description Convert special characters between three ```
* @param {string[]} splitWithCodeTag Array of HTML strings separated by three ```
* @returns {string} HTML in which special characters between three ``` have been converted
*/
export function escapeHtmlCharactersInCodeTag(splitWithCodeTag) {
for (let index = 0; index < splitWithCodeTag.length; index++) {
const codeTagRequired =
splitWithCodeTag[index] !== '```' && index < splitWithCodeTag.length - 1
if (codeTagRequired) {
splitWithCodeTag.splice(index + 1, 0, '```')
}
}
let inCodeTag = false
let result = ''
for (let content of splitWithCodeTag) {
if (content === '```') {
inCodeTag = !inCodeTag
} else if (inCodeTag) {
content = escapeHtmlCharacters(content)
}
result += content
}
return result
}

View File

@@ -0,0 +1,103 @@
import attachmentManagement from './attachmentManagement'
import yaml from 'js-yaml'
import path from 'path'
const delimiterRegExp = /^\-{3}/
/**
* ```
* {
* storagePath,
* export
* }
* ```
*/
export default function formatMarkdown(props) {
return function(note, targetPath, exportTasks) {
let result = note.content
if (props.storagePath && note.key) {
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
result,
props.storagePath
)
const destinationFolder = props.export.prefixAttachmentFolder
? `${path.parse(targetPath).name} - ${
attachmentManagement.DESTINATION_FOLDER
}`
: attachmentManagement.DESTINATION_FOLDER
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: destinationFolder
})
})
result = attachmentManagement.replaceStorageReferences(
result,
note.key,
destinationFolder
)
}
if (props.export.metadata === 'MERGE_HEADER') {
const metadata = getFrontMatter(result)
const values = Object.assign({}, note)
delete values.content
delete values.isTrashed
for (const key in values) {
metadata[key] = values[key]
}
result = replaceFrontMatter(result, metadata)
} else if (props.export.metadata === 'MERGE_VARIABLE') {
const metadata = getFrontMatter(result)
const values = Object.assign({}, note)
delete values.content
delete values.isTrashed
if (props.export.variable) {
metadata[props.export.variable] = values
} else {
for (const key in values) {
metadata[key] = values[key]
}
}
result = replaceFrontMatter(result, metadata)
}
return result
}
}
function getFrontMatter(markdown) {
const lines = markdown.split('\n')
if (delimiterRegExp.test(lines[0])) {
let line = 0
while (++line < lines.length && !delimiterRegExp.test(lines[line])) {}
return yaml.load(lines.slice(1, line).join('\n')) || {}
} else {
return {}
}
}
function replaceFrontMatter(markdown, metadata) {
const lines = markdown.split('\n')
if (delimiterRegExp.test(lines[0])) {
let line = 0
while (++line < lines.length && !delimiterRegExp.test(lines[line])) {}
return `---\n${yaml.dump(metadata)}---\n${lines.slice(line + 1).join('\n')}`
} else {
return `---\n${yaml.dump(metadata)}---\n\n${markdown}`
}
}

View File

@@ -0,0 +1,26 @@
import formatHTML from './formatHTML'
import { remote } from 'electron'
export default function formatPDF(props) {
return function(note, targetPath, exportTasks) {
const printout = new remote.BrowserWindow({
show: false,
webPreferences: { webSecurity: false, javascript: false }
})
printout.loadURL(
'data:text/html;charset=UTF-8,' +
formatHTML(props)(note, targetPath, exportTasks)
)
return new Promise((resolve, reject) => {
printout.webContents.on('did-finish-load', () => {
printout.webContents.printToPDF({}, (err, data) => {
if (err) reject(err)
else resolve(data)
printout.destroy()
})
})
})
}
}

View File

@@ -0,0 +1,58 @@
import formatMarkdown from './formatMarkdown'
import formatHTML from './formatHTML'
import formatPDF from './formatPDF'
/**
* @param {Object} storage
* @param {String} fileType
* @param {Object} config
*/
export default function getContentFormatter(storage, fileType, config) {
if (fileType === 'md') {
return formatMarkdown({
storagePath: storage.path,
export: config.export
})
} else if (fileType === 'html') {
return formatHTML({
theme: config.ui.theme,
fontSize: config.preview.fontSize,
fontFamily: config.preview.fontFamily,
codeBlockTheme: config.preview.codeBlockTheme,
codeBlockFontFamily: config.editor.fontFamily,
lineNumber: config.preview.lineNumber,
indentSize: config.editor.indentSize,
scrollPastEnd: config.preview.scrollPastEnd,
smartQuotes: config.preview.smartQuotes,
breaks: config.preview.breaks,
sanitize: config.preview.sanitize,
customCSS: config.preview.customCSS,
allowCustomCSS: config.preview.allowCustomCSS,
storagePath: storage.path,
export: config.export,
RTL: config.editor.rtlEnabled /* && this.state.RTL */
})
} else if (fileType === 'pdf') {
return formatPDF({
theme: config.ui.theme,
fontSize: config.preview.fontSize,
fontFamily: config.preview.fontFamily,
codeBlockTheme: config.preview.codeBlockTheme,
codeBlockFontFamily: config.editor.fontFamily,
lineNumber: config.preview.lineNumber,
indentSize: config.editor.indentSize,
scrollPastEnd: config.preview.scrollPastEnd,
smartQuotes: config.preview.smartQuotes,
breaks: config.preview.breaks,
sanitize: config.preview.sanitize,
customCSS: config.preview.customCSS,
allowCustomCSS: config.preview.allowCustomCSS,
storagePath: storage.path,
export: config.export,
RTL: config.editor.rtlEnabled /* && this.state.RTL */
})
}
return null
}

View File

@@ -0,0 +1,37 @@
import filenamify from 'filenamify'
import i18n from 'browser/lib/i18n'
import path from 'path'
/**
* @param {Object} note
* @param {String} fileType
* @param {String} directory
* @param {Object} deduplicator
*
* @return {String}
*/
function getFilename(note, fileType, directory, deduplicator) {
const basename = note.title
? filenamify(note.title, { replacement: '_' })
: i18n.__('Untitled')
if (deduplicator) {
if (deduplicator[basename]) {
const filename = path.join(
directory,
`${basename} (${deduplicator[basename]}).${fileType}`
)
++deduplicator[basename]
return filename
} else {
deduplicator[basename] = 1
}
}
return path.join(directory, `${basename}.${fileType}`)
}
module.exports = getFilename

View File

@@ -15,11 +15,14 @@ const dataApi = {
updateNote: require('./updateNote'), updateNote: require('./updateNote'),
deleteNote: require('./deleteNote'), deleteNote: require('./deleteNote'),
moveNote: require('./moveNote'), moveNote: require('./moveNote'),
exportNoteAs: require('./exportNoteAs'),
migrateFromV5Storage: require('./migrateFromV5Storage'), migrateFromV5Storage: require('./migrateFromV5Storage'),
createSnippet: require('./createSnippet'), createSnippet: require('./createSnippet'),
deleteSnippet: require('./deleteSnippet'), deleteSnippet: require('./deleteSnippet'),
updateSnippet: require('./updateSnippet'), updateSnippet: require('./updateSnippet'),
fetchSnippet: require('./fetchSnippet'), fetchSnippet: require('./fetchSnippet'),
exportTag: require('./exportTag'),
getFilename: require('./getFilename'),
_migrateFromV6Storage: require('./migrateFromV6Storage'), _migrateFromV6Storage: require('./migrateFromV6Storage'),
_resolveStorageData: require('./resolveStorageData'), _resolveStorageData: require('./resolveStorageData'),

View File

@@ -1,5 +1,109 @@
@import('./Tab') @import('./Tab')
.container
display flex
flex-direction column
align-items center
justify-content center
position relative
margin-bottom 2em
margin-left 2em
.box-minmax
width 608px
height 45px
display flex
justify-content space-between
font-size $tab--button-font-size
color $ui-text-color
span first-child
margin-top 18px
padding-right 10px
padding-left 10px
padding-top 8px
position relative
border $ui-borderColor
border-radius 5px
background $ui-backgroundColor
div[id^="secondRow"]
position absolute
z-index 2
left 0
top 0
margin-bottom -42px
.rs-label
margin-left -20px
div[id^="firstRow"]
position absolute
z-index 2
left 0
top 0
margin-bottom -25px
.rs-range
&::-webkit-slider-thumb
margin-top 0px
transform rotate(180deg)
.rs-label
margin-bottom -85px
margin-top 85px
.rs-range
margin-top 29px
width 600px
-webkit-appearance none
&:focus
outline black
&::-webkit-slider-runnable-track
width 100%
height 0.1px
cursor pointer
box-shadow none
background $ui-backgroundColor
border-radius 0px
border 0px solid #010101
cursor none
&::-webkit-slider-thumb
box-shadow none
border 1px solid $ui-borderColor
box-shadow 0px 10px 10px rgba(0, 0, 0, 0.25)
height 32px
width 32px
border-radius 22px
background white
cursor pointer
-webkit-appearance none
margin-top -20px
border-color $ui-default-button-backgroundColor
height 32px
border-top-left-radius 10%
border-top-right-radius 10%
.rs-label
position relative
transform-origin center center
display block
background transparent
border-radius none
line-height 30px
font-weight normal
box-sizing border-box
border none
margin-bottom -5px
margin-top -10px
clear both
float left
height 17px
margin-left -25px
left attr(value)
color $ui-text-color
font-style normal
font-weight normal
line-height normal
font-size $tab--button-font-size
.root .root
padding 15px padding 15px
margin-bottom 30px margin-bottom 30px
@@ -35,6 +139,13 @@
margin-right 10px margin-right 10px
font-size 14px font-size 14px
.group-section-label-right
width 200px
text-align right
margin-right 10px
font-size 14px
padding-right 1.5rem
.group-section-control .group-section-control
flex 1 flex 1
margin-left 5px margin-left 5px
@@ -175,6 +286,9 @@ body[data-theme="dark"]
.group-section-control .group-section-control
select, .group-section-control-input select, .group-section-control-input
colorDarkControl() colorDarkControl()
.rs-label
color $ui-dark-text-color
apply-theme(theme) apply-theme(theme)
body[data-theme={theme}] body[data-theme={theme}]
@@ -205,6 +319,8 @@ apply-theme(theme)
.group-section-control .group-section-control
select, .group-section-control-input select, .group-section-control-input
colorThemedControl(theme) colorThemedControl(theme)
.rs-label
color get-theme-var(theme, 'text-color')
for theme in 'solarized-dark' 'dracula' for theme in 'solarized-dark' 'dracula'
apply-theme(theme) apply-theme(theme)

View File

@@ -0,0 +1,184 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ConfigTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
const electron = require('electron')
const ipc = electron.ipcRenderer
class ExportTab extends React.Component {
constructor(props) {
super(props)
this.state = {
config: props.config
}
}
clearMessage() {
_.debounce(() => {
this.setState({
ExportAlert: null
})
}, 2000)()
}
componentDidMount() {
this.handleSettingDone = () => {
this.setState({
ExportAlert: {
type: 'success',
message: i18n.__('Successfully applied!')
}
})
}
this.handleSettingError = err => {
this.setState({
ExportAlert: {
type: 'error',
message:
err.message != null ? err.message : i18n.__('An error occurred!')
}
})
}
this.oldExport = this.state.config.export
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
ipc.addListener('APP_SETTING_ERROR', this.handleSettingError)
}
componentWillUnmount() {
ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone)
ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError)
}
handleSaveButtonClick(e) {
const newConfig = {
export: this.state.config.export
}
ConfigManager.set(newConfig)
store.dispatch({
type: 'SET_UI',
config: newConfig
})
this.clearMessage()
this.props.haveToSave()
}
handleExportChange(e) {
const { config } = this.state
config.export = {
metadata: this.refs.metadata.value,
variable: !_.isNil(this.refs.variable)
? this.refs.variable.value
: config.export.variable,
prefixAttachmentFolder: this.refs.prefixAttachmentFolder.checked
}
this.setState({
config
})
if (_.isEqual(this.oldExport, config.export)) {
this.props.haveToSave()
} else {
this.props.haveToSave({
tab: 'Export',
type: 'warning',
message: i18n.__('Unsaved Changes!')
})
}
}
render() {
const { config, ExportAlert } = this.state
const ExportAlertElement =
ExportAlert != null ? (
<p className={`alert ${ExportAlert.type}`}>{ExportAlert.message}</p>
) : null
return (
<div styleName='root'>
<div styleName='group'>
<div styleName='group-header'>{i18n.__('Export')}</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Metadata')}</div>
<div styleName='group-section-control'>
<select
value={config.export.metadata}
onChange={e => this.handleExportChange(e)}
ref='metadata'
>
<option value='DONT_EXPORT'>{i18n.__(`Don't export`)}</option>
<option value='MERGE_HEADER'>
{i18n.__('Merge with the header')}
</option>
<option value='MERGE_VARIABLE'>
{i18n.__('Merge with a variable')}
</option>
</select>
</div>
</div>
{config.export.metadata === 'MERGE_VARIABLE' && (
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Variable Name')}
</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
onChange={e => this.handleExportChange(e)}
ref='variable'
value={config.export.variable}
type='text'
/>
</div>
</div>
)}
<div styleName='group-checkBoxSection'>
<label>
<input
onChange={e => this.handleExportChange(e)}
checked={config.export.prefixAttachmentFolder}
ref='prefixAttachmentFolder'
type='checkbox'
/>
&nbsp;
{i18n.__('Prefix attachment folder')}
</label>
</div>
<div styleName='group-control'>
<button
styleName='group-control-rightButton'
onClick={e => this.handleSaveButtonClick(e)}
>
{i18n.__('Save')}
</button>
{ExportAlertElement}
</div>
</div>
</div>
)
}
}
ExportTab.propTypes = {
dispatch: PropTypes.func,
haveToSave: PropTypes.func
}
export default CSSModules(ExportTab, styles)

View File

@@ -16,25 +16,78 @@ class InfoTab extends React.Component {
super(props) super(props)
this.state = { this.state = {
config: this.props.config config: this.props.config,
subscriptionFormStatus: 'idle',
subscriptionFormErrorMessage: null,
subscriptionFormEmail: ''
} }
} }
componentDidMount() {
const { autoUpdateEnabled, amaEnabled } = ConfigManager.get()
this.setState({ config: { autoUpdateEnabled, amaEnabled } })
}
handleLinkClick(e) { handleLinkClick(e) {
shell.openExternal(e.currentTarget.href) shell.openExternal(e.currentTarget.href)
e.preventDefault() e.preventDefault()
} }
handleConfigChange(e) { handleConfigChange(e) {
const newConfig = { amaEnabled: this.refs.amaEnabled.checked } const newConfig = {
amaEnabled: this.refs.amaEnabled.checked,
autoUpdateEnabled: this.refs.autoUpdateEnabled.checked
}
this.setState({ config: newConfig }) this.setState({ config: newConfig })
return newConfig
}
handleSubscriptionFormSubmit(e) {
e.preventDefault()
this.setState({
subscriptionFormStatus: 'sending',
subscriptionFormErrorMessage: null
})
fetch(
'https://boostmails.boostio.co/api/public/lists/5f434dccd05f3160b41c0d49/subscriptions',
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
method: 'POST',
body: JSON.stringify({ email: this.state.subscriptionFormEmail })
}
)
.then(response => {
if (response.status >= 400) {
return response.text().then(text => {
throw new Error(text)
})
}
this.setState({
subscriptionFormStatus: 'done'
})
})
.catch(error => {
this.setState({
subscriptionFormStatus: 'idle',
subscriptionFormErrorMessage: error.message
})
})
}
handleSubscriptionFormEmailChange(e) {
this.setState({
subscriptionFormEmail: e.target.value
})
} }
handleSaveButtonClick(e) { handleSaveButtonClick(e) {
const newConfig = { const newConfig = this.state.config
amaEnabled: this.state.config.amaEnabled
}
if (!newConfig.amaEnabled) { if (!newConfig.amaEnabled) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('DISABLE_AMA') AwsMobileAnalyticsConfig.recordDynamicCustomEvent('DISABLE_AMA')
@@ -61,20 +114,17 @@ class InfoTab extends React.Component {
}) })
} }
toggleAutoUpdate() {
const newConfig = {
autoUpdateEnabled: !this.state.config.autoUpdateEnabled
}
this.setState({ config: newConfig })
ConfigManager.set(newConfig)
}
infoMessage() { infoMessage() {
const { amaMessage } = this.state const { amaMessage } = this.state
return amaMessage ? <p styleName='policy-confirm'>{amaMessage}</p> : null return amaMessage ? <p styleName='policy-confirm'>{amaMessage}</p> : null
} }
handleAutoUpdateChange() {
const { autoUpdateEnabled } = this.handleConfigChange()
ConfigManager.set({ autoUpdateEnabled })
}
render() { render() {
return ( return (
<div styleName='root'> <div styleName='root'>
@@ -134,6 +184,40 @@ class InfoTab extends React.Component {
<hr /> <hr />
<div styleName='group-header--sub'>Subscribe Update Notes</div>
{this.state.subscriptionFormStatus === 'done' ? (
<div>
<blockquote color={{ color: 'green' }}>
Thanks for the subscription!
</blockquote>
</div>
) : (
<div>
{this.state.subscriptionFormErrorMessage != null && (
<blockquote style={{ color: 'red' }}>
{this.state.subscriptionFormErrorMessage}
</blockquote>
)}
<form onSubmit={e => this.handleSubscriptionFormSubmit(e)}>
<input
styleName='subscription-email-input'
placeholder='E-mail'
type='email'
onChange={e => this.handleSubscriptionFormEmailChange(e)}
disabled={this.state.subscriptionFormStatus === 'sending'}
/>
<button
styleName='subscription-submit-button'
type='submit'
disabled={this.state.subscriptionFormStatus === 'sending'}
>
Subscribe
</button>
</form>
</div>
)}
<hr />
<div styleName='group-header--sub'>{i18n.__('About')}</div> <div styleName='group-header--sub'>{i18n.__('About')}</div>
<div styleName='top'> <div styleName='top'>
@@ -145,9 +229,7 @@ class InfoTab extends React.Component {
height='92' height='92'
/> />
<div styleName='icon-right'> <div styleName='icon-right'>
<div styleName='appId'> <div styleName='appId'>Boostnote Legacy {appVersion}</div>
{i18n.__('Boostnote')} {appVersion}
</div>
<div styleName='description'> <div styleName='description'>
{i18n.__( {i18n.__(
'An open source note-taking app made for programmers just like you.' 'An open source note-taking app made for programmers just like you.'
@@ -183,7 +265,8 @@ class InfoTab extends React.Component {
<label> <label>
<input <input
type='checkbox' type='checkbox'
onChange={this.toggleAutoUpdate.bind(this)} ref='autoUpdateEnabled'
onChange={() => this.handleAutoUpdateChange()}
checked={this.state.config.autoUpdateEnabled} checked={this.state.config.autoUpdateEnabled}
/> />
{i18n.__('Enable Auto Update')} {i18n.__('Enable Auto Update')}

View File

@@ -33,6 +33,35 @@
.separate-line .separate-line
margin 40px 0 margin 40px 0
.subscription-email-input
height 35px
vertical-align middle
width 200px
font-size $tab--button-font-size
border solid 1px $border-color
border-radius 2px
padding 0 5px
margin-right 5px
outline none
&:disabled
background-color $ui-input--disabled-backgroundColor
.subscription-submit-button
margin-top 10px
height 35px
border-radius 2px
border none
background-color alpha(#1EC38B, 90%)
padding-left 20px
padding-right 20px
text-decoration none
color white
font-weight 600
font-size 16px
&:hover
background-color #1EC38B
transition 0.2s
.policy-submit .policy-submit
margin-top 10px margin-top 10px
height 35px height 35px

View File

@@ -0,0 +1,207 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ConfigTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager'
import { store } from 'browser/main/store'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
import { sync as commandExists } from 'command-exists'
const electron = require('electron')
const ipc = electron.ipcRenderer
const { remote } = electron
const { dialog } = remote
class PluginsTab extends React.Component {
constructor(props) {
super(props)
this.state = {
config: props.config
}
}
componentDidMount() {
this.handleSettingDone = () => {
this.setState({
pluginsAlert: {
type: 'success',
message: i18n.__('Successfully applied!')
}
})
}
this.handleSettingError = err => {
this.setState({
pluginsAlert: {
type: 'error',
message:
err.message != null ? err.message : i18n.__('An error occurred!')
}
})
}
this.oldWakatimeConfig = this.state.config.wakatime
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
ipc.addListener('APP_SETTING_ERROR', this.handleSettingError)
}
componentWillUnmount() {
ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone)
ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError)
}
checkWakatimePluginRequirement() {
const { wakatime } = this.state.config
if (wakatime.isActive && !commandExists('wakatime')) {
this.setState({
wakatimePluginAlert: {
type: i18n.__('Warning'),
message: i18n.__('Missing wakatime cli')
}
})
const alertConfig = {
type: 'warning',
message: i18n.__('Missing Wakatime CLI'),
detail: i18n.__(
`Please install Wakatime CLI to use Wakatime tracker feature.`
),
buttons: [i18n.__('OK')]
}
dialog.showMessageBox(remote.getCurrentWindow(), alertConfig)
} else {
this.setState({
wakatimePluginAlert: null
})
}
}
handleSaveButtonClick(e) {
const newConfig = {
wakatime: {
isActive: this.state.config.wakatime.isActive,
key: this.state.config.wakatime.key
}
}
ConfigManager.set(newConfig)
store.dispatch({
type: 'SET_CONFIG',
config: newConfig
})
this.clearMessage()
this.props.haveToSave()
this.checkWakatimePluginRequirement()
}
handleIsWakatimePluginActiveChange(e) {
const { config } = this.state
config.wakatime.isActive = !config.wakatime.isActive
this.setState({
config
})
if (_.isEqual(this.oldWakatimeConfig.isActive, config.wakatime.isActive)) {
this.props.haveToSave()
} else {
this.props.haveToSave({
tab: 'Plugins',
type: 'warning',
message: i18n.__('Unsaved Changes!')
})
}
}
handleWakatimeKeyChange(e) {
const { config } = this.state
config.wakatime = {
isActive: true,
key: this.refs.wakatimeKey.value
}
this.setState({
config
})
if (_.isEqual(this.oldWakatimeConfig.key, config.wakatime.key)) {
this.props.haveToSave()
} else {
this.props.haveToSave({
tab: 'Plugins',
type: 'warning',
message: i18n.__('Unsaved Changes!')
})
}
}
clearMessage() {
_.debounce(() => {
this.setState({
pluginsAlert: null
})
}, 2000)()
}
render() {
const pluginsAlert = this.state.pluginsAlert
const pluginsAlertElement =
pluginsAlert != null ? (
<p className={`alert ${pluginsAlert.type}`}>{pluginsAlert.message}</p>
) : null
const wakatimeAlert = this.state.wakatimePluginAlert
const wakatimePluginAlertElement =
wakatimeAlert != null ? (
<p className={`alert ${wakatimeAlert.type}`}>{wakatimeAlert.message}</p>
) : null
const { config } = this.state
return (
<div styleName='root'>
<div styleName='group'>
<div styleName='group-header'>{i18n.__('Plugins')}</div>
<div styleName='group-header2'>{i18n.__('Wakatime')}</div>
<div styleName='group-checkBoxSection'>
<label>
<input
onChange={e => this.handleIsWakatimePluginActiveChange(e)}
checked={config.wakatime.isActive}
ref='wakatimeIsActive'
type='checkbox'
/>
&nbsp;
{i18n.__('Enable Wakatime')}
</label>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Wakatime key')}</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
onChange={e => this.handleWakatimeKeyChange(e)}
disabled={!config.wakatime.isActive}
ref='wakatimeKey'
value={config.wakatime.key}
type='text'
/>
{wakatimePluginAlertElement}
</div>
</div>
<div styleName='group-control'>
<button
styleName='group-control-rightButton'
onClick={e => this.handleSaveButtonClick(e)}
>
{i18n.__('Save')}
</button>
{pluginsAlertElement}
</div>
</div>
</div>
)
}
}
PluginsTab.propTypes = {
dispatch: PropTypes.func,
haveToSave: PropTypes.func
}
export default CSSModules(PluginsTab, styles)

View File

@@ -35,10 +35,18 @@ class SnippetEditor extends React.Component {
foldGutter: true, foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
autoCloseBrackets: { autoCloseBrackets: {
pairs: this.props.matchingPairs, codeBlock: {
triples: this.props.matchingTriples, pairs: this.props.codeBlockMatchingPairs,
explode: this.props.explodingPairs, closeBefore: this.props.codeBlockMatchingCloseBefore,
override: true triples: this.props.codeBlockMatchingTriples,
explode: this.props.codeBlockExplodingPairs
},
markdown: {
pairs: this.props.matchingPairs,
closeBefore: this.props.matchingCloseBefore,
triples: this.props.matchingTriples,
explode: this.props.explodingPairs
}
}, },
mode: 'null' mode: 'null'
}) })

View File

@@ -152,8 +152,15 @@ class SnippetTab extends React.Component {
rulers={config.editor.rulers} rulers={config.editor.rulers}
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
matchingPairs={config.editor.matchingPairs} matchingPairs={config.editor.matchingPairs}
matchingCloseBefore={config.editor.matchingCloseBefore}
matchingTriples={config.editor.matchingTriples} matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs} explodingPairs={config.editor.explodingPairs}
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
codeBlockMatchingCloseBefore={
config.editor.codeBlockMatchingCloseBefore
}
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
onRef={ref => { onRef={ref => {
this.snippetEditor = ref this.snippetEditor = ref

View File

@@ -13,6 +13,7 @@ import i18n from 'browser/lib/i18n'
import { getLanguages } from 'browser/lib/Languages' import { getLanguages } from 'browser/lib/Languages'
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
import uiThemes from 'browser/lib/ui-themes' import uiThemes from 'browser/lib/ui-themes'
import { chooseTheme, applyTheme } from 'browser/main/lib/ThemeManager'
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
@@ -84,6 +85,11 @@ class UiTab extends React.Component {
const newConfig = { const newConfig = {
ui: { ui: {
theme: this.refs.uiTheme.value, theme: this.refs.uiTheme.value,
defaultTheme: this.refs.uiTheme.value,
enableScheduleTheme: this.refs.enableScheduleTheme.checked,
scheduledTheme: this.refs.uiScheduledTheme.value,
scheduleStart: this.refs.scheduleStart.value,
scheduleEnd: this.refs.scheduleEnd.value,
language: this.refs.uiLanguage.value, language: this.refs.uiLanguage.value,
defaultNote: this.refs.defaultNote.value, defaultNote: this.refs.defaultNote.value,
tagNewNoteWithFilteringTags: this.refs.tagNewNoteWithFilteringTags tagNewNoteWithFilteringTags: this.refs.tagNewNoteWithFilteringTags
@@ -118,16 +124,24 @@ class UiTab extends React.Component {
enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked, enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked,
frontMatterTitleField: this.refs.frontMatterTitleField.value, frontMatterTitleField: this.refs.frontMatterTitleField.value,
matchingPairs: this.refs.matchingPairs.value, matchingPairs: this.refs.matchingPairs.value,
matchingCloseBefore: this.refs.matchingCloseBefore.value,
matchingTriples: this.refs.matchingTriples.value, matchingTriples: this.refs.matchingTriples.value,
explodingPairs: this.refs.explodingPairs.value, explodingPairs: this.refs.explodingPairs.value,
codeBlockMatchingPairs: this.refs.codeBlockMatchingPairs.value,
codeBlockMatchingCloseBefore: this.refs.codeBlockMatchingCloseBefore
.value,
codeBlockMatchingTriples: this.refs.codeBlockMatchingTriples.value,
codeBlockExplodingPairs: this.refs.codeBlockExplodingPairs.value,
spellcheck: this.refs.spellcheck.checked, spellcheck: this.refs.spellcheck.checked,
enableSmartPaste: this.refs.enableSmartPaste.checked, enableSmartPaste: this.refs.enableSmartPaste.checked,
enableMarkdownLint: this.refs.enableMarkdownLint.checked, enableMarkdownLint: this.refs.enableMarkdownLint.checked,
customMarkdownLintConfig: this.customMarkdownLintConfigCM customMarkdownLintConfig: this.customMarkdownLintConfigCM
.getCodeMirror() .getCodeMirror()
.getValue(), .getValue(),
dateFormatISO8601: this.refs.dateFormatISO8601.checked,
prettierConfig: this.prettierConfigCM.getCodeMirror().getValue(), prettierConfig: this.prettierConfigCM.getCodeMirror().getValue(),
deleteUnusedAttachments: this.refs.deleteUnusedAttachments.checked deleteUnusedAttachments: this.refs.deleteUnusedAttachments.checked,
rtlEnabled: this.refs.rtlEnabled.checked
}, },
preview: { preview: {
fontSize: this.refs.previewFontSize.value, fontSize: this.refs.previewFontSize.value,
@@ -189,6 +203,9 @@ class UiTab extends React.Component {
preview: this.state.config.preview preview: this.state.config.preview
} }
chooseTheme(newConfig)
applyTheme(newConfig.ui.theme)
ConfigManager.set(newConfig) ConfigManager.set(newConfig)
store.dispatch({ store.dispatch({
@@ -207,6 +224,21 @@ class UiTab extends React.Component {
}, 2000)() }, 2000)()
} }
formatTime(time) {
let hour = Math.floor(time / 60)
let minute = time % 60
if (hour < 10) {
hour = '0' + hour
}
if (minute < 10) {
minute = '0' + minute
}
return `${hour}:${minute}`
}
render() { render() {
const UiAlert = this.state.UiAlert const UiAlert = this.state.UiAlert
const UiAlertElement = const UiAlertElement =
@@ -231,7 +263,7 @@ class UiTab extends React.Component {
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<select <select
value={config.ui.theme} value={config.ui.defaultTheme}
onChange={e => this.handleUIChange(e)} onChange={e => this.handleUIChange(e)}
ref='uiTheme' ref='uiTheme'
> >
@@ -262,6 +294,101 @@ class UiTab extends React.Component {
</select> </select>
</div> </div>
</div> </div>
<div styleName='group-header2'>{i18n.__('Theme Schedule')}</div>
<div styleName='group-checkBoxSection'>
<label>
<input
onChange={e => this.handleUIChange(e)}
checked={config.ui.enableScheduleTheme}
ref='enableScheduleTheme'
type='checkbox'
/>
&nbsp;
{i18n.__('Enable Scheduled Themes')}
</label>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Scheduled Theme')}
</div>
<div styleName='group-section-control'>
<select
disabled={!config.ui.enableScheduleTheme}
value={config.ui.scheduledTheme}
onChange={e => this.handleUIChange(e)}
ref='uiScheduledTheme'
>
<optgroup label='Light Themes'>
{uiThemes
.filter(theme => !theme.isDark)
.sort((a, b) => a.label.localeCompare(b.label))
.map(theme => {
return (
<option value={theme.name} key={theme.name}>
{theme.label}
</option>
)
})}
</optgroup>
<optgroup label='Dark Themes'>
{uiThemes
.filter(theme => theme.isDark)
.sort((a, b) => a.label.localeCompare(b.label))
.map(theme => {
return (
<option value={theme.name} key={theme.name}>
{theme.label}
</option>
)
})}
</optgroup>
</select>
</div>
</div>
<div styleName='group-section'>
<div styleName='container'>
<div id='firstRow'>
<span
id='rs-bullet-1'
styleName='rs-label'
>{`End: ${this.formatTime(config.ui.scheduleEnd)}`}</span>
<input
disabled={!config.ui.enableScheduleTheme}
id='rs-range-line-1'
styleName='rs-range'
type='range'
value={config.ui.scheduleEnd}
min='0'
max='1440'
step='5'
ref='scheduleEnd'
onChange={e => this.handleUIChange(e)}
/>
</div>
<div id='secondRow'>
<span
id='rs-bullet-2'
styleName='rs-label'
>{`Start: ${this.formatTime(config.ui.scheduleStart)}`}</span>
<input
disabled={!config.ui.enableScheduleTheme}
id='rs-range-line-2'
styleName='rs-range'
type='range'
value={config.ui.scheduleStart}
min='0'
max='1440'
step='5'
ref='scheduleStart'
onChange={e => this.handleUIChange(e)}
/>
</div>
<div styleName='box-minmax'>
<span>00:00</span>
<span>24:00</span>
</div>
</div>
</div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Language')}</div> <div styleName='group-section-label'>{i18n.__('Language')}</div>
@@ -625,6 +752,126 @@ class UiTab extends React.Component {
</div> </div>
</div> </div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Matching character pairs')}
</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
value={this.state.config.editor.matchingPairs}
ref='matchingPairs'
onChange={e => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label-right'>
{i18n.__('in code blocks')}
</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
value={this.state.config.editor.codeBlockMatchingPairs}
ref='codeBlockMatchingPairs'
onChange={e => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Close pairs before')}
</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
value={this.state.config.editor.matchingCloseBefore}
ref='matchingCloseBefore'
onChange={e => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label-right'>
{i18n.__('in code blocks')}
</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
value={this.state.config.editor.codeBlockMatchingCloseBefore}
ref='codeBlockMatchingCloseBefore'
onChange={e => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Matching character triples')}
</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
value={this.state.config.editor.matchingTriples}
ref='matchingTriples'
onChange={e => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label-right'>
{i18n.__('in code blocks')}
</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
value={this.state.config.editor.codeBlockMatchingTriples}
ref='codeBlockMatchingTriples'
onChange={e => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Exploding character pairs')}
</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
value={this.state.config.editor.explodingPairs}
ref='explodingPairs'
onChange={e => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label-right'>
{i18n.__('in code blocks')}
</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
value={this.state.config.editor.codeBlockExplodingPairs}
ref='codeBlockExplodingPairs'
onChange={e => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-checkBoxSection'> <div styleName='group-checkBoxSection'>
<label> <label>
<input <input
@@ -742,51 +989,32 @@ class UiTab extends React.Component {
)} )}
</label> </label>
</div> </div>
<div styleName='group-checkBoxSection'>
<div styleName='group-section'> <label>
<div styleName='group-section-label'>
{i18n.__('Matching character pairs')}
</div>
<div styleName='group-section-control'>
<input <input
styleName='group-section-control-input'
value={this.state.config.editor.matchingPairs}
ref='matchingPairs'
onChange={e => this.handleUIChange(e)} onChange={e => this.handleUIChange(e)}
type='text' checked={this.state.config.editor.rtlEnabled}
ref='rtlEnabled'
type='checkbox'
/> />
</div> &nbsp;
{i18n.__('Enable right to left direction(RTL)')}
</label>
</div> </div>
<div styleName='group-section'> <div styleName='group-checkBoxSection'>
<div styleName='group-section-label'> <label>
{i18n.__('Matching character triples')}
</div>
<div styleName='group-section-control'>
<input <input
styleName='group-section-control-input'
value={this.state.config.editor.matchingTriples}
ref='matchingTriples'
onChange={e => this.handleUIChange(e)} onChange={e => this.handleUIChange(e)}
type='text' checked={this.state.config.editor.dateFormatISO8601}
ref='dateFormatISO8601'
type='checkbox'
/> />
</div> &nbsp;
{i18n.__('Date shortcut use iso 8601 format')}
</label>
</div> </div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Exploding character pairs')}
</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
value={this.state.config.editor.explodingPairs}
ref='explodingPairs'
onChange={e => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
{i18n.__('Custom MarkdownLint Rules')} {i18n.__('Custom MarkdownLint Rules')}

View File

@@ -6,7 +6,9 @@ import UiTab from './UiTab'
import InfoTab from './InfoTab' import InfoTab from './InfoTab'
import Crowdfunding from './Crowdfunding' import Crowdfunding from './Crowdfunding'
import StoragesTab from './StoragesTab' import StoragesTab from './StoragesTab'
import ExportTab from './ExportTab'
import SnippetTab from './SnippetTab' import SnippetTab from './SnippetTab'
import PluginsTab from './PluginsTab'
import Blog from './Blog' import Blog from './Blog'
import ModalEscButton from 'browser/components/ModalEscButton' import ModalEscButton from 'browser/components/ModalEscButton'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
@@ -23,7 +25,8 @@ class Preferences extends React.Component {
currentTab: 'STORAGES', currentTab: 'STORAGES',
UIAlert: '', UIAlert: '',
HotkeyAlert: '', HotkeyAlert: '',
BlogAlert: '' BlogAlert: '',
ExportAlert: ''
} }
} }
@@ -80,8 +83,25 @@ class Preferences extends React.Component {
haveToSave={alert => this.setState({ BlogAlert: alert })} haveToSave={alert => this.setState({ BlogAlert: alert })}
/> />
) )
case 'EXPORT':
return (
<ExportTab
dispatch={dispatch}
config={config}
data={data}
haveToSave={alert => this.setState({ ExportAlert: alert })}
/>
)
case 'SNIPPET': case 'SNIPPET':
return <SnippetTab dispatch={dispatch} config={config} data={data} /> return <SnippetTab dispatch={dispatch} config={config} data={data} />
case 'PLUGINS':
return (
<PluginsTab
dispatch={dispatch}
config={config}
haveToSave={alert => this.setState({ PluginsAlert: alert })}
/>
)
case 'STORAGES': case 'STORAGES':
default: default:
return ( return (
@@ -122,7 +142,13 @@ class Preferences extends React.Component {
{ target: 'INFO', label: i18n.__('About') }, { target: 'INFO', label: i18n.__('About') },
{ target: 'CROWDFUNDING', label: i18n.__('Crowdfunding') }, { target: 'CROWDFUNDING', label: i18n.__('Crowdfunding') },
{ target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert }, { target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert },
{ target: 'SNIPPET', label: i18n.__('Snippets') } {
target: 'EXPORT',
label: i18n.__('Export'),
Export: this.state.ExportAlert
},
{ target: 'SNIPPET', label: i18n.__('Snippets') },
{ target: 'PLUGINS', label: i18n.__('Plugins') }
] ]
const navButtons = tabs.map(tab => { const navButtons = tabs.map(tab => {

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './RenameFolderModal.styl' import styles from './RenameModal.styl'
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
import { store } from 'browser/main/store' import { store } from 'browser/main/store'
import ModalEscButton from 'browser/components/ModalEscButton' import ModalEscButton from 'browser/components/ModalEscButton'

View File

@@ -46,13 +46,18 @@
font-size 14px font-size 14px
colorPrimaryButton() colorPrimaryButton()
.error
text-align center
color #F44336
height 20px
apply-theme(theme) apply-theme(theme)
body[data-theme={theme}] body[data-theme={theme}]
.root .root
background-color transparent background-color transparent
.header .header
background-color get-theme-var(theme, 'button--hover-backgroundColor') background-color transparent
border-color get-theme-var(theme, 'borderColor') border-color get-theme-var(theme, 'borderColor')
color get-theme-var(theme, 'text-color') color get-theme-var(theme, 'text-color')

View File

@@ -0,0 +1,196 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './RenameModal.styl'
import dataApi from 'browser/main/lib/dataApi'
import ModalEscButton from 'browser/components/ModalEscButton'
import i18n from 'browser/lib/i18n'
import { replace } from 'connected-react-router'
import ee from 'browser/main/lib/eventEmitter'
import { isEmpty } from 'lodash'
import electron from 'electron'
const { remote } = electron
const { dialog } = remote
class RenameTagModal extends React.Component {
constructor(props) {
super(props)
this.nameInput = null
this.handleChange = this.handleChange.bind(this)
this.setTextInputRef = el => {
this.nameInput = el
}
this.state = {
name: props.tagName,
oldName: props.tagName
}
}
componentDidMount() {
this.nameInput.focus()
this.nameInput.select()
}
handleChange(e) {
this.setState({
name: this.nameInput.value,
showerror: false,
errormessage: ''
})
}
handleKeyDown(e) {
if (e.keyCode === 27) {
this.props.close()
}
}
handleInputKeyDown(e) {
switch (e.keyCode) {
case 13:
this.handleConfirm()
}
}
handleConfirm() {
if (this.state.name.trim().length > 0) {
const { name, oldName } = this.state
this.renameTag(oldName, name)
}
}
showError(message) {
this.setState({
showerror: true,
errormessage: message
})
}
renameTag(tag, updatedTag) {
const { data, dispatch } = this.props
if (tag === updatedTag) {
// confirm with-out any change - just dismiss the modal
this.props.close()
return
}
if (
data.noteMap
.map(note => note)
.some(note => note.tags.indexOf(updatedTag) !== -1)
) {
const alertConfig = {
type: 'warning',
message: i18n.__('Confirm tag merge'),
detail: i18n.__(
`Tag ${tag} will be merged with existing tag ${updatedTag}`
),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}
const dialogButtonIndex = dialog.showMessageBox(
remote.getCurrentWindow(),
alertConfig
)
if (dialogButtonIndex === 1) {
return // bail early on cancel click
}
}
const notes = data.noteMap
.map(note => note)
.filter(
note => note.tags.indexOf(tag) !== -1 && note.tags.indexOf(updatedTag)
)
.map(note => {
note = Object.assign({}, note)
note.tags = note.tags.slice()
note.tags[note.tags.indexOf(tag)] = updatedTag
return note
})
if (isEmpty(notes)) {
this.showError(i18n.__('Tag exists'))
return
}
Promise.all(
notes.map(note => dataApi.updateNote(note.storage, note.key, note))
)
.then(updatedNotes => {
updatedNotes.forEach(note => {
dispatch({
type: 'UPDATE_NOTE',
note
})
})
})
.then(() => {
if (window.location.hash.includes(tag)) {
dispatch(replace(`/tags/${updatedTag}`))
}
ee.emit('sidebar:rename-tag', { tag, updatedTag })
this.props.close()
})
}
render() {
const { close } = this.props
const { errormessage } = this.state
return (
<div
styleName='root'
tabIndex='-1'
onKeyDown={e => this.handleKeyDown(e)}
>
<div styleName='header'>
<div styleName='title'>{i18n.__('Rename Tag')}</div>
</div>
<ModalEscButton handleEscButtonClick={close} />
<div styleName='control'>
<input
styleName='control-input'
placeholder={i18n.__('Tag Name')}
ref={this.setTextInputRef}
value={this.state.name}
onChange={this.handleChange}
onKeyDown={e => this.handleInputKeyDown(e)}
/>
<button
styleName='control-confirmButton'
onClick={() => this.handleConfirm()}
>
{i18n.__('Confirm')}
</button>
</div>
<div className='error' styleName='error'>
{errormessage}
</div>
</div>
)
}
}
RenameTagModal.propTypes = {
storage: PropTypes.shape({
key: PropTypes.string
}),
folder: PropTypes.shape({
key: PropTypes.string,
name: PropTypes.string
})
}
export default CSSModules(RenameTagModal, styles)

View File

@@ -0,0 +1,196 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
var defaults = {
pairs: "()[]{}''\"\"",
closeBefore: ")]}'\":;>",
triples: "",
explode: "[]{}"
};
var Pos = CodeMirror.Pos;
CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
cm.removeKeyMap(keyMap);
cm.state.closeBrackets = null;
}
if (val) {
ensureBound(getOption(val.markdown, "pairs"))
cm.state.closeBrackets = val;
cm.addKeyMap(keyMap);
}
});
function getOption(conf, name) {
if (name == "pairs" && typeof conf == "string") return conf;
if (typeof conf == "object" && conf[name] != null) return conf[name];
return defaults[name];
}
var keyMap = {Backspace: handleBackspace, Enter: handleEnter};
function ensureBound(chars) {
for (var i = 0; i < chars.length; i++) {
var ch = chars.charAt(i), key = "'" + ch + "'"
if (!keyMap[key]) keyMap[key] = handler(ch)
}
}
ensureBound(defaults.pairs + "`")
function handler(ch) {
return function(cm) { return handleChar(cm, ch); };
}
function getConfig(cm) {
var cursor = cm.getCursor();
var token = cm.getTokenAt(cursor);
var inCodeBlock = !!token.state.fencedEndRE;
if (inCodeBlock) {
return cm.state.closeBrackets.codeBlock
} else {
return cm.state.closeBrackets.markdown
}
}
function handleBackspace(cm) {
var conf = getConfig(cm);
if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
var pairs = getOption(conf, "pairs");
var ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
if (!ranges[i].empty()) return CodeMirror.Pass;
var around = charsAround(cm, ranges[i].head);
if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
}
for (var i = ranges.length - 1; i >= 0; i--) {
var cur = ranges[i].head;
cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete");
}
}
function handleEnter(cm) {
var conf = getConfig(cm);
var explode = conf && getOption(conf, "explode");
if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass;
var ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
if (!ranges[i].empty()) return CodeMirror.Pass;
var around = charsAround(cm, ranges[i].head);
if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
}
cm.operation(function() {
var linesep = cm.lineSeparator() || "\n";
cm.replaceSelection(linesep + linesep, null);
cm.execCommand("goCharLeft");
ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
var line = ranges[i].head.line;
cm.indentLine(line, null, true);
cm.indentLine(line + 1, null, true);
}
});
}
function contractSelection(sel) {
var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0;
return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)),
head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))};
}
function handleChar(cm, ch) {
var conf = getConfig(cm);
if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
var pairs = getOption(conf, "pairs");
var pos = pairs.indexOf(ch);
if (pos == -1) return CodeMirror.Pass;
var closeBefore = getOption(conf,"closeBefore");
var triples = getOption(conf, "triples");
var identical = pairs.charAt(pos + 1) == ch;
var ranges = cm.listSelections();
var opening = pos % 2 == 0;
var type;
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i], cur = range.head, curType;
var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
if (opening && !range.empty()) {
curType = "surround";
} else if ((identical || !opening) && next == ch) {
if (identical && stringStartsAfter(cm, cur))
curType = "both";
else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
curType = "skipThree";
else
curType = "skip";
} else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 &&
cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch) {
if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass;
curType = "addFour";
} else if (identical) {
var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur)
if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both";
else return CodeMirror.Pass;
} else if (opening && (next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)) {
curType = "both";
} else {
return CodeMirror.Pass;
}
if (!type) type = curType;
else if (type != curType) return CodeMirror.Pass;
}
var left = pos % 2 ? pairs.charAt(pos - 1) : ch;
var right = pos % 2 ? ch : pairs.charAt(pos + 1);
cm.operation(function() {
if (type == "skip") {
cm.execCommand("goCharRight");
} else if (type == "skipThree") {
for (var i = 0; i < 3; i++)
cm.execCommand("goCharRight");
} else if (type == "surround") {
var sels = cm.getSelections();
for (var i = 0; i < sels.length; i++)
sels[i] = left + sels[i] + right;
cm.replaceSelections(sels, "around");
sels = cm.listSelections().slice();
for (var i = 0; i < sels.length; i++)
sels[i] = contractSelection(sels[i]);
cm.setSelections(sels);
} else if (type == "both") {
cm.replaceSelection(left + right, null);
cm.triggerElectric(left + right);
cm.execCommand("goCharLeft");
} else if (type == "addFour") {
cm.replaceSelection(left + left + left + left, "before");
cm.execCommand("goCharRight");
}
});
}
function charsAround(cm, pos) {
var str = cm.getRange(Pos(pos.line, pos.ch - 1),
Pos(pos.line, pos.ch + 1));
return str.length == 2 ? str : null;
}
function stringStartsAfter(cm, pos) {
var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1))
return /\bstring/.test(token.type) && token.start == pos.ch &&
(pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos)))
}
});

View File

@@ -1,15 +1,24 @@
(function (mod) { ;(function(mod) {
if (typeof exports === 'object' && typeof module === 'object') { // Common JS if (typeof exports === 'object' && typeof module === 'object') {
// Common JS
mod(require('../codemirror/lib/codemirror')) mod(require('../codemirror/lib/codemirror'))
} else if (typeof define === 'function' && define.amd) { // AMD } else if (typeof define === 'function' && define.amd) {
// AMD
define(['../codemirror/lib/codemirror'], mod) define(['../codemirror/lib/codemirror'], mod)
} else { // Plain browser env } else {
// Plain browser env
mod(CodeMirror) mod(CodeMirror)
} }
})(function (CodeMirror) { })(function(CodeMirror) {
'use strict' 'use strict'
const shell = require('electron').shell const shell = require('electron').shell
const remote = require('electron').remote
const eventEmitter = {
emit: function() {
remote.getCurrentWindow().webContents.send.apply(null, arguments)
}
}
const yOffset = 2 const yOffset = 2
const macOS = global.process.platform === 'darwin' const macOS = global.process.platform === 'darwin'
@@ -28,11 +37,16 @@
this.tooltip = document.createElement('div') this.tooltip = document.createElement('div')
this.tooltipContent = document.createElement('div') this.tooltipContent = document.createElement('div')
this.tooltipIndicator = document.createElement('div') this.tooltipIndicator = document.createElement('div')
this.tooltip.setAttribute('class', 'CodeMirror-hover CodeMirror-matchingbracket CodeMirror-selected') this.tooltip.setAttribute(
'class',
'CodeMirror-hover CodeMirror-matchingbracket CodeMirror-selected'
)
this.tooltip.setAttribute('cm-ignore-events', 'true') this.tooltip.setAttribute('cm-ignore-events', 'true')
this.tooltip.appendChild(this.tooltipContent) this.tooltip.appendChild(this.tooltipContent)
this.tooltip.appendChild(this.tooltipIndicator) this.tooltip.appendChild(this.tooltipIndicator)
this.tooltipContent.textContent = `${macOS ? 'Cmd(⌘)' : 'Ctrl(^)'} + click to follow link` this.tooltipContent.textContent = `${
macOS ? 'Cmd(⌘)' : 'Ctrl(^)'
} + click to follow link`
this.lineDiv.addEventListener('mousedown', this.onMouseDown) this.lineDiv.addEventListener('mousedown', this.onMouseDown)
this.lineDiv.addEventListener('mouseenter', this.onMouseEnter, { this.lineDiv.addEventListener('mouseenter', this.onMouseEnter, {
@@ -51,7 +65,16 @@
const className = el.className.split(' ') const className = el.className.split(' ')
if (className.indexOf('cm-url') !== -1) { if (className.indexOf('cm-url') !== -1) {
const match = /^\((.*)\)|\[(.*)\]|(.*)$/.exec(el.textContent) // multiple cm-url because of search term
const cmUrlSpans = Array.from(
el.parentNode.getElementsByClassName('cm-url')
)
const textContent =
cmUrlSpans.length > 1
? cmUrlSpans.map(span => span.textContent).join('')
: el.textContent
const match = /^\((.*)\)|\[(.*)\]|(.*)$/.exec(textContent)
const url = match[1] || match[2] || match[3] const url = match[1] || match[2] || match[3]
// `:storage` is the value of the variable `STORAGE_FOLDER_PLACEHOLDER` defined in `browser/main/lib/dataApi/attachmentManagement` // `:storage` is the value of the variable `STORAGE_FOLDER_PLACEHOLDER` defined in `browser/main/lib/dataApi/attachmentManagement`
@@ -60,13 +83,90 @@
return null return null
} }
specialLinkHandler(e, rawHref, linkHash) {
const isStartWithHash = rawHref[0] === '#'
const extractIdRegex = /file:\/\/.*main.?\w*.html#/ // file://path/to/main(.development.)html
const regexNoteInternalLink = new RegExp(`${extractIdRegex.source}(.+)`)
if (isStartWithHash || regexNoteInternalLink.test(rawHref)) {
const posOfHash = linkHash.indexOf('#')
if (posOfHash > -1) {
const extractedId = linkHash.slice(posOfHash + 1)
const targetId = mdurl.encode(extractedId)
const targetElement = document.getElementById(targetId) // this.getWindow().document.getElementById(targetId)
if (targetElement != null) {
this.scrollTo(0, targetElement.offsetTop)
}
return
}
}
// this will match the new uuid v4 hash and the old hash
// e.g.
// :note:1c211eb7dcb463de6490 and
// :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c
const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/
if (regexIsNoteLink.test(linkHash)) {
eventEmitter.emit('list:jump', linkHash.replace(':note:', ''))
return
}
const regexIsLine = /^:line:[0-9]/
if (regexIsLine.test(linkHash)) {
const numberPattern = /\d+/g
const lineNumber = parseInt(linkHash.match(numberPattern)[0])
eventEmitter.emit('line:jump', lineNumber)
return
}
// this will match the old link format storage.key-note.key
// e.g.
// 877f99c3268608328037-1c211eb7dcb463de6490
const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/
if (regexIsLegacyNoteLink.test(linkHash)) {
eventEmitter.emit('list:jump', linkHash.split('-')[1])
return
}
const regexIsTagLink = /^:tag:([\w]+)$/
if (regexIsTagLink.test(rawHref)) {
const tag = rawHref.match(regexIsTagLink)[1]
eventEmitter.emit('dispatch:push', `/tags/${encodeURIComponent(tag)}`)
return
}
}
onMouseDown(e) { onMouseDown(e) {
const { target } = e const { target } = e
if (!e[modifier]) { if (!e[modifier]) {
return return
} }
// Create URL spans array used for special case "search term is hitting a link".
const cmUrlSpans = Array.from(
e.target.parentNode.getElementsByClassName('cm-url')
)
const innerText =
cmUrlSpans.length > 1
? cmUrlSpans.map(span => span.textContent).join('')
: e.target.innerText
const rawHref = innerText.trim().slice(1, -1) // get link text from markdown text
if (!rawHref) return // not checked href because parser will create file://... string for [empty link]()
const parser = document.createElement('a')
parser.href = rawHref
const { href, hash } = parser
const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10
this.specialLinkHandler(target, rawHref, linkHash)
const url = this.getUrl(target) const url = this.getUrl(target)
// all special cases handled --> other case
if (url) { if (url) {
e.preventDefault() e.preventDefault()
@@ -79,9 +179,11 @@
const url = this.getUrl(target) const url = this.getUrl(target)
if (url) { if (url) {
if (e[modifier]) { if (e[modifier]) {
target.classList.add('CodeMirror-activeline-background', 'CodeMirror-hyperlink') target.classList.add(
} 'CodeMirror-activeline-background',
else { 'CodeMirror-hyperlink'
)
} else {
target.classList.add('CodeMirror-activeline-background') target.classList.add('CodeMirror-activeline-background')
} }
@@ -90,7 +192,10 @@
} }
onMouseLeave(e) { onMouseLeave(e) {
if (this.tooltip.parentElement === this.lineDiv) { if (this.tooltip.parentElement === this.lineDiv) {
e.target.classList.remove('CodeMirror-activeline-background', 'CodeMirror-hyperlink') e.target.classList.remove(
'CodeMirror-activeline-background',
'CodeMirror-hyperlink'
)
this.lineDiv.removeChild(this.tooltip) this.lineDiv.removeChild(this.tooltip)
} }
@@ -99,8 +204,7 @@
if (this.tooltip.parentElement === this.lineDiv) { if (this.tooltip.parentElement === this.lineDiv) {
if (e[modifier]) { if (e[modifier]) {
e.target.classList.add('CodeMirror-hyperlink') e.target.classList.add('CodeMirror-hyperlink')
} } else {
else {
e.target.classList.remove('CodeMirror-hyperlink') e.target.classList.remove('CodeMirror-hyperlink')
} }
} }
@@ -110,21 +214,20 @@
const b2 = this.lineDiv.getBoundingClientRect() const b2 = this.lineDiv.getBoundingClientRect()
const tdiv = this.tooltip const tdiv = this.tooltip
tdiv.style.left = (b1.left - b2.left) + 'px' tdiv.style.left = b1.left - b2.left + 'px'
this.lineDiv.appendChild(tdiv) this.lineDiv.appendChild(tdiv)
const b3 = tdiv.getBoundingClientRect() const b3 = tdiv.getBoundingClientRect()
const top = b1.top - b2.top - b3.height - yOffset const top = b1.top - b2.top - b3.height - yOffset
if (top < 0) { if (top < 0) {
tdiv.style.top = (b1.top - b2.top + b1.height + yOffset) + 'px' tdiv.style.top = b1.top - b2.top + b1.height + yOffset + 'px'
} } else {
else {
tdiv.style.top = top + 'px' tdiv.style.top = top + 'px'
} }
} }
} }
CodeMirror.defineOption('hyperlink', true, (cm) => { CodeMirror.defineOption('hyperlink', true, cm => {
const addon = new HyperLink(cm) const addon = new HyperLink(cm)
}) })
}) })

View File

@@ -1,10 +1,20 @@
(function(mod) { ;(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS if (typeof exports == 'object' && typeof module == 'object')
mod(require("../codemirror/lib/codemirror"), require("../codemirror/mode/gfm/gfm"), require("../codemirror/mode/yaml-frontmatter/yaml-frontmatter")) // CommonJS
else if (typeof define == "function" && define.amd) // AMD mod(
define(["../codemirror/lib/codemirror", "../codemirror/mode/gfm/gfm", "../codemirror/mode/yaml-frontmatter/yaml-frontmatter"], mod) require('../codemirror/lib/codemirror'),
else // Plain browser env require('../codemirror/mode/gfm/gfm'),
mod(CodeMirror) require('../codemirror/mode/yaml-frontmatter/yaml-frontmatter')
)
else if (typeof define == 'function' && define.amd)
// AMD
define([
'../codemirror/lib/codemirror',
'../codemirror/mode/gfm/gfm',
'../codemirror/mode/yaml-frontmatter/yaml-frontmatter'
], mod)
// Plain browser env
else mod(CodeMirror)
})(function(CodeMirror) { })(function(CodeMirror) {
'use strict' 'use strict'
@@ -87,18 +97,23 @@
token: function(stream, state) { token: function(stream, state) {
const initialPos = stream.pos const initialPos = stream.pos
if (state.fencedEndRE && stream.match(state.fencedEndRE)) { if (state.fencedEndRE) {
state.fencedEndRE = null if (stream.match(state.fencedEndRE)) {
state.fencedMode = null state.fencedEndRE = null
state.fencedState = null state.fencedMode = null
state.fencedState = null
stream.pos = initialPos stream.pos = initialPos
} else if (state.fencedMode) {
return state.fencedMode.token(stream, state.fencedState)
} else {
state.overlayCur = this.overlayToken(stream, state)
state.overlayPos = stream.pos
return state.overlayCur
}
} }
else { else {
if (state.fencedMode) {
return state.fencedMode.token(stream, state.fencedState)
}
const match = stream.match(fencedCodeRE, true) const match = stream.match(fencedCodeRE, true)
if (match) { if (match) {
state.fencedEndRE = new RegExp(match[1] + '+ *$') state.fencedEndRE = new RegExp(match[1] + '+ *$')
@@ -141,30 +156,10 @@
overlayToken: function(stream, state) { overlayToken: function(stream, state) {
state.combineTokens = false state.combineTokens = false
if (state.fencedEndRE && stream.match(state.fencedEndRE)) {
state.fencedEndRE = null
state.localMode = null
state.localState = null
return null
}
if (state.localMode) { if (state.localMode) {
return state.localMode.token(stream, state.localState) || '' return state.localMode.token(stream, state.localState) || ''
} }
const match = stream.match(fencedCodeRE, true)
if (match) {
state.fencedEndRE = new RegExp(match[1] + '+ *$')
state.localMode = getMode(match[2], match[3], config, stream.lineOracle.doc.cm)
if (state.localMode) {
state.localState = CodeMirror.startState(state.localMode)
}
return null
}
state.combineTokens = true state.combineTokens = true
if (state.inTable) { if (state.inTable) {
@@ -226,8 +221,8 @@
CodeMirror.defineMIME('text/x-bfm', 'bfm') CodeMirror.defineMIME('text/x-bfm', 'bfm')
CodeMirror.modeInfo.push({ CodeMirror.modeInfo.push({
name: "Boost Flavored Markdown", name: 'Boost Flavored Markdown',
mime: "text/x-bfm", mime: 'text/x-bfm',
mode: "bfm" mode: 'bfm'
}) })
}) })

157
extra_scripts/codemirror/mode/gfm/gfm.js vendored Normal file
View File

@@ -0,0 +1,157 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
;(function(mod) {
if (typeof exports == 'object' && typeof module == 'object')
// CommonJS
mod(
require('../codemirror/lib/codemirror'),
require('../codemirror/mode/markdown/markdown'),
require('../codemirror/addon/mode/overlay')
)
else if (typeof define == 'function' && define.amd)
// AMD
define([
'../codemirror/lib/codemirror',
'../codemirror/mode/markdown/markdown',
'../codemirror/addon/mode/overlay'
], mod)
// Plain browser env
else mod(CodeMirror)
})(function(CodeMirror) {
'use strict'
var urlRE = /^((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/i
CodeMirror.defineMode(
'gfm',
function(config, modeConfig) {
var codeDepth = 0
function blankLine(state) {
state.code = false
return null
}
var gfmOverlay = {
startState: function() {
return {
code: false,
codeBlock: false,
ateSpace: false
}
},
copyState: function(s) {
return {
code: s.code,
codeBlock: s.codeBlock,
ateSpace: s.ateSpace
}
},
token: function(stream, state) {
state.combineTokens = null
// Hack to prevent formatting override inside code blocks (block and inline)
if (state.codeBlock) {
if (stream.match(/^```+/)) {
state.codeBlock = false
return null
}
stream.skipToEnd()
return null
}
if (stream.sol()) {
state.code = false
}
if (stream.sol() && stream.match(/^```+/)) {
stream.skipToEnd()
state.codeBlock = true
return null
}
// If this block is changed, it may need to be updated in Markdown mode
if (stream.peek() === '`') {
stream.next()
var before = stream.pos
stream.eatWhile('`')
var difference = 1 + stream.pos - before
if (!state.code) {
codeDepth = difference
state.code = true
} else {
if (difference === codeDepth) {
// Must be exact
state.code = false
}
}
return null
} else if (state.code) {
stream.next()
return null
}
// Check if space. If so, links can be formatted later on
if (stream.eatSpace()) {
state.ateSpace = true
return null
}
if (stream.sol() || state.ateSpace) {
state.ateSpace = false
if (modeConfig.gitHubSpice !== false) {
if (
stream.match(
/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?=.{0,6}\d)(?:[a-f0-9]{7,40}\b)/
)
) {
// User/Project@SHA
// User@SHA
// SHA
state.combineTokens = true
return 'link'
} else if (
stream.match(
/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/
)
) {
// User/Project#Num
// User#Num
// #Num
state.combineTokens = true
return 'link'
}
}
}
if (
stream.match(urlRE) &&
stream.string.slice(stream.start - 2, stream.start) != '](' &&
(stream.start == 0 ||
/\W/.test(stream.string.charAt(stream.start - 1)))
) {
// URLs
// Taken from http://daringfireball.net/2010/07/improved_regex_for_matching_urls
// And then (issue #1160) simplified to make it not crash the Chrome Regexp engine
// And then limited url schemes to the CommonMark list, so foo:bar isn't matched as a URL
state.combineTokens = true
return 'link'
}
stream.next()
return null
},
blankLine: blankLine
}
var markdownConfig = {
taskLists: true,
strikethrough: true,
emoji: true
}
for (var attr in modeConfig) {
markdownConfig[attr] = modeConfig[attr]
}
markdownConfig.name = 'markdown'
return CodeMirror.overlayMode(
CodeMirror.getMode(config, markdownConfig),
gfmOverlay
)
},
'markdown'
)
CodeMirror.defineMIME('text/x-gfm', 'gfm')
})

View File

@@ -187,7 +187,7 @@ module.exports = function(grunt) {
} }
ChildProcess.exec( ChildProcess.exec(
`codesign --verbose --deep --force --sign \"${OSX_COMMON_NAME}\" dist/Boostnote-darwin-x64/Boostnote.app`, `codesign --verbose --deep --force --timestamp=none --sign \"${OSX_COMMON_NAME}\" dist/Boostnote-darwin-x64/Boostnote.app`,
function(err, stdout, stderr) { function(err, stdout, stderr) {
grunt.log.writeln(stdout) grunt.log.writeln(stdout)
if (err) { if (err) {

View File

@@ -26,6 +26,7 @@ if (!singleInstance) {
} }
var isUpdateReady = false var isUpdateReady = false
let updateFound = false
var ghReleasesOpts = { var ghReleasesOpts = {
repo: 'BoostIO/boost-releases', repo: 'BoostIO/boost-releases',
@@ -36,25 +37,33 @@ const updater = new GhReleases(ghReleasesOpts)
// Check for updates // Check for updates
// `status` returns true if there is a new update available // `status` returns true if there is a new update available
function checkUpdate() { function checkUpdate(manualTriggered = false) {
if (!isPackaged) { if (!isPackaged) {
// Prevents app from attempting to update when in dev mode. // Prevents app from attempting to update when in dev mode.
console.log('Updates are disabled in Development mode, see main-app.js') console.log('Updates are disabled in Development mode, see main-app.js')
return true return true
} }
if (!electronConfig.get('autoUpdateEnabled', true)) return
if (process.platform === 'linux' || isUpdateReady) { // End if auto updates disabled and it is an automatic check
if (!electronConfig.get('autoUpdateEnabled', true) && !manualTriggered) return
if (process.platform === 'linux' || isUpdateReady || updateFound) {
return true return true
} }
updater.check((err, status) => { updater.check((err, status) => {
if (err) { if (err) {
var isLatest = err.message === 'There is no newer version.' var isLatest = err.message === 'There is no newer version.'
if (!isLatest) console.error('Updater error! %s', err.message) if (!isLatest) console.error('Updater error! %s', err.message)
mainWindow.webContents.send(
'update-not-found',
isLatest ? 'There is no newer version.' : 'Updater error'
)
return return
} }
if (status) { if (status) {
mainWindow.webContents.send('update-found', 'Update available!') mainWindow.webContents.send('update-found', 'Update available!')
updater.download() updateFound = true
} }
}) })
} }
@@ -63,6 +72,7 @@ updater.on('update-downloaded', info => {
if (mainWindow != null) { if (mainWindow != null) {
mainWindow.webContents.send('update-ready', 'Update available!') mainWindow.webContents.send('update-ready', 'Update available!')
isUpdateReady = true isUpdateReady = true
updateFound = false
} }
}) })
@@ -77,6 +87,14 @@ ipc.on('update-app-confirm', function(event, msg) {
} }
}) })
ipc.on('update-cancel', () => {
updateFound = false
})
ipc.on('update-download-confirm', () => {
updater.download()
})
app.on('window-all-closed', function() { app.on('window-all-closed', function() {
app.quit() app.quit()
}) })
@@ -113,7 +131,7 @@ app.on('ready', function() {
if (isUpdateReady) { if (isUpdateReady) {
mainWindow.webContents.send('update-ready', 'Update available!') mainWindow.webContents.send('update-ready', 'Update available!')
} else { } else {
checkUpdate() checkUpdate(msg === 'manual')
} }
}) })
}, 10 * 1000) }, 10 * 1000)

View File

@@ -178,6 +178,18 @@ const file = {
mainWindow.webContents.send('list:isMarkdownNote', 'print') mainWindow.webContents.send('list:isMarkdownNote', 'print')
mainWindow.webContents.send('print') mainWindow.webContents.send('print')
} }
},
{
type: 'separator'
},
{
label: 'Update',
click() {
mainWindow.webContents.send('update')
}
},
{
type: 'separator'
} }
] ]
} }
@@ -314,6 +326,12 @@ const view = {
mainWindow.webContents.send('editor:fullscreen') mainWindow.webContents.send('editor:fullscreen')
} }
}, },
{
label: 'Toggle Editor Orientation',
click() {
mainWindow.webContents.send('editor:orientation')
}
},
{ {
type: 'separator' type: 'separator'
}, },
@@ -466,9 +484,21 @@ const help = {
] ]
} }
const team = {
label: 'For Team',
submenu: [
{
label: 'BoostHub',
click: async () => {
shell.openExternal('https://boosthub.io/')
}
}
]
}
module.exports = module.exports =
process.platform === 'darwin' process.platform === 'darwin'
? [boost, file, edit, view, window, help] ? [boost, file, edit, view, window, team, help]
: process.platform === 'win32' : process.platform === 'win32'
? [boost, file, view, help] ? [boost, file, view, team, help]
: [file, view, help] : [file, view, team, help]

View File

@@ -72,7 +72,7 @@
border-left-color: rgba(142, 142, 142, 0.5); border-left-color: rgba(142, 142, 142, 0.5);
mix-blend-mode: difference; mix-blend-mode: difference;
} }
.CodeMirror-scroll { .CodeMirror-scroll {
margin-bottom: 0; margin-bottom: 0;
padding-bottom: 0; padding-bottom: 0;
@@ -108,15 +108,15 @@
<script src="../node_modules/codemirror/addon/display/panel.js"></script> <script src="../node_modules/codemirror/addon/display/panel.js"></script>
<script src="../node_modules/codemirror/mode/xml/xml.js"></script> <script src="../node_modules/codemirror/mode/xml/xml.js"></script>
<script src="../node_modules/codemirror/mode/markdown/markdown.js"></script> <script src="../node_modules/codemirror/mode/markdown/markdown.js"></script>
<script src="../node_modules/codemirror/mode/gfm/gfm.js"></script>
<script src="../node_modules/codemirror/mode/yaml/yaml.js"></script> <script src="../node_modules/codemirror/mode/yaml/yaml.js"></script>
<script src="../node_modules/codemirror/mode/yaml-frontmatter/yaml-frontmatter.js"></script> <script src="../node_modules/codemirror/mode/yaml-frontmatter/yaml-frontmatter.js"></script>
<script src="../extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js"></script> <script src="../extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js"></script>
<script src="../extra_scripts/codemirror/mode/bfm/bfm.js"></script> <script src="../extra_scripts/codemirror/mode/bfm/bfm.js"></script>
<script src="../extra_scripts/codemirror/mode/gfm/gfm.js"></script>
<script src="../extra_scripts/codemirror/addon/hyperlink/hyperlink.js"></script> <script src="../extra_scripts/codemirror/addon/hyperlink/hyperlink.js"></script>
<script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script> <script src="../extra_scripts/codemirror/addon/edit/closebrackets.js"></script>
<script src="../node_modules/codemirror/addon/edit/matchbrackets.js"></script> <script src="../node_modules/codemirror/addon/edit/matchbrackets.js"></script>
<script src="../node_modules/codemirror/addon/search/search.js"></script> <script src="../node_modules/codemirror/addon/search/search.js"></script>

View File

@@ -104,15 +104,15 @@
<script src="../node_modules/codemirror/addon/display/panel.js"></script> <script src="../node_modules/codemirror/addon/display/panel.js"></script>
<script src="../node_modules/codemirror/mode/xml/xml.js"></script> <script src="../node_modules/codemirror/mode/xml/xml.js"></script>
<script src="../node_modules/codemirror/mode/markdown/markdown.js"></script> <script src="../node_modules/codemirror/mode/markdown/markdown.js"></script>
<script src="../node_modules/codemirror/mode/gfm/gfm.js"></script>
<script src="../node_modules/codemirror/mode/yaml/yaml.js"></script> <script src="../node_modules/codemirror/mode/yaml/yaml.js"></script>
<script src="../node_modules/codemirror/mode/yaml-frontmatter/yaml-frontmatter.js"></script> <script src="../node_modules/codemirror/mode/yaml-frontmatter/yaml-frontmatter.js"></script>
<script src="../extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js"></script> <script src="../extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js"></script>
<script src="../extra_scripts/codemirror/mode/bfm/bfm.js"></script> <script src="../extra_scripts/codemirror/mode/bfm/bfm.js"></script>
<script src="../extra_scripts/codemirror/mode/gfm/gfm.js"></script>
<script src="../extra_scripts/codemirror/addon/hyperlink/hyperlink.js"></script> <script src="../extra_scripts/codemirror/addon/hyperlink/hyperlink.js"></script>
<script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script> <script src="../extra_scripts/codemirror/addon/edit/closebrackets.js"></script>
<script src="../node_modules/codemirror/addon/edit/matchbrackets.js"></script> <script src="../node_modules/codemirror/addon/edit/matchbrackets.js"></script>
<script src="../node_modules/codemirror/addon/search/search.js"></script> <script src="../node_modules/codemirror/addon/search/search.js"></script>

View File

@@ -202,7 +202,6 @@
"Create new folder": "Ordner erstellen", "Create new folder": "Ordner erstellen",
"Folder name": "Ordnername", "Folder name": "Ordnername",
"Create": "Erstellen", "Create": "Erstellen",
"Untitled": "Neuer Ordner",
"Unlink Storage": "Speicherverknüpfung aufheben", "Unlink Storage": "Speicherverknüpfung aufheben",
"Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.": "Die Verknüpfung des Speichers mit Boostnote wird entfernt. Es werden keine Daten gelöscht. Um die Daten dauerhaft zu löschen musst du den Ordner auf der Festplatte manuell entfernen.", "Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.": "Die Verknüpfung des Speichers mit Boostnote wird entfernt. Es werden keine Daten gelöscht. Um die Daten dauerhaft zu löschen musst du den Ordner auf der Festplatte manuell entfernen.",
"Empty note": "Leere Notiz", "Empty note": "Leere Notiz",

View File

@@ -7,6 +7,7 @@
"Ctrl(^)": "Ctrl(^)", "Ctrl(^)": "Ctrl(^)",
"to create a new note": "新增筆記", "to create a new note": "新增筆記",
"Toggle Mode": "切換模式", "Toggle Mode": "切換模式",
"Add tag...": "新增標籤...",
"Trash": "垃圾桶", "Trash": "垃圾桶",
"Ok": "好", "Ok": "好",
"MODIFICATION DATE": "修改時間", "MODIFICATION DATE": "修改時間",
@@ -22,50 +23,57 @@
".pdf": ".pdf", ".pdf": ".pdf",
"Print": "列印", "Print": "列印",
"Your preferences for Boostnote": "Boostnote 偏好設定", "Your preferences for Boostnote": "Boostnote 偏好設定",
"Help": "幫助",
"Hide Help": "隱藏幫助",
"Storage Locations": "儲存空間", "Storage Locations": "儲存空間",
"Add Storage Location": "新增儲存位置", "Add Storage Location": "新增儲存位置",
"Add Folder": "新增資料夾", "Add Folder": "新增資料夾",
"Select Folder": "選擇資料夾",
"Open Storage folder": "開啟儲存資料夾", "Open Storage folder": "開啟儲存資料夾",
"Unlink": "解除連結", "Unlink": "解除連結",
"Edit": "編輯", "Edit": "編輯",
"Delete": "刪除", "Delete": "刪除",
"Interface": "面", "Interface": "面",
"Interface Theme": "主題", "Interface Theme": "介面主題",
"Default": "預設", "Default": "預設",
"White": "White", "White": "White",
"Solarized Dark": "Solarized Dark", "Solarized Dark": "Solarized Dark",
"Dark": "Dark", "Dark": "Dark",
"Show a confirmation dialog when deleting notes": "刪除筆記的時候,顯示確認對話框", "Show a confirmation dialog when deleting notes": "刪除筆記顯示確認對話框",
"Disable Direct Write (It will be applied after restarting)": "停用直接編輯 (重新啟動後生效)",
"Show only related tags": "只顯示相關標籤",
"Editor Theme": "編輯器主題", "Editor Theme": "編輯器主題",
"Editor Font Size": "編輯器字型大小", "Editor Font Size": "編輯器字型大小",
"Editor Font Family": "編輯器字體", "Editor Font Family": "編輯器字體",
"Editor Indent Style": "縮排風格", "Editor Indent Style": "縮排風格",
"Spaces": "空格", "Spaces": "空格",
"Tabs": "Tabs", "Tabs": "Tabs",
"Switch to Preview": "切回預覽頁面的時機", "Switch to Preview": "切回預覽頁面",
"When Editor Blurred": "當編輯器失去焦點時", "When Editor Blurred": "當編輯器失去焦點時",
"When Editor Blurred, Edit On Double Click": "當編輯器失去焦點時,雙擊切換到編輯畫面", "When Editor Blurred, Edit On Double Click": "當編輯器失去焦點時,點兩下開始編輯",
"On Right Click": "點選右鍵切換兩個頁面", "On Right Click": "點選右鍵",
"Editor Keymap": "編輯器 Keymap", "Editor Keymap": "編輯器 Keymap",
"default": "預設", "default": "預設",
"vim": "vim", "vim": "vim",
"emacs": "emacs", "emacs": "emacs",
"⚠️ Please restart boostnote after you change the keymap": "⚠️ 修改鍵盤配置請重新啟 Boostnote ", "⚠️ Please restart boostnote after you change the keymap": "⚠️ 修改鍵盤配置請重新啟 Boostnote ",
"Show line numbers in the editor": "在編輯器中顯示行號", "Show line numbers in the editor": "在編輯器中顯示行號",
"Allow editor to scroll past the last line": "允許編輯器捲軸捲動超過最後一行", "Allow editor to scroll past the last line": "允許編輯器捲軸捲動超過最後一行",
"Bring in web page title when pasting URL on editor": "在編輯器貼上網址的時候,自動加上網頁標題", "Enable smart quotes": "啟用智慧引號",
"Bring in web page title when pasting URL on editor": "在編輯器貼上網址時自動加上網頁標題",
"Preview": "預覽頁面", "Preview": "預覽頁面",
"Preview Font Size": "預覽頁面字型大小", "Preview Font Size": "預覽頁面字型大小",
"Preview Font Family": "預覽頁面字體", "Preview Font Family": "預覽頁面字體",
"Code Block Theme": "程式碼區塊主題", "Code Block Theme": "程式碼區塊主題",
"Allow preview to scroll past the last line": "允許預覽頁面捲軸捲動超過最後一行", "Allow preview to scroll past the last line": "允許預覽頁面捲軸捲動超過最後一行",
"Show line numbers for preview code blocks": "在預覽頁面的程式碼區塊中顯示行號", "Show line numbers for preview code blocks": "在程式碼區塊預覽中顯示行號",
"LaTeX Inline Open Delimiter": "LaTeX 單行開頭符號", "LaTeX Inline Open Delimiter": "LaTeX 單行開頭符號",
"LaTeX Inline Close Delimiter": "LaTeX 單行結尾符號", "LaTeX Inline Close Delimiter": "LaTeX 單行結尾符號",
"LaTeX Block Open Delimiter": "LaTeX 多行開頭符號", "LaTeX Block Open Delimiter": "LaTeX 多行開頭符號",
"LaTeX Block Close Delimiter": "LaTeX 多行結尾符號", "LaTeX Block Close Delimiter": "LaTeX 多行結尾符號",
"PlantUML Server": "PlantUML 伺服器",
"Community": "社群", "Community": "社群",
"Subscribe to Newsletter": "訂閱郵件", "Subscribe to Newsletter": "訂閱電子報",
"GitHub": "GitHub", "GitHub": "GitHub",
"Blog": "部落格", "Blog": "部落格",
"Facebook Group": "Facebook 社團", "Facebook Group": "Facebook 社團",
@@ -73,34 +81,35 @@
"About": "關於", "About": "關於",
"Boostnote": "Boostnote", "Boostnote": "Boostnote",
"An open source note-taking app made for programmers just like you.": "一款專門為程式設計師朋友量身打造的開源筆記軟體", "An open source note-taking app made for programmers just like you.": "一款專門為程式設計師朋友量身打造的開源筆記軟體",
"Website": "官", "Website": "官方網站",
"Development": "開發", "Development": "開發",
" : Development configurations for Boostnote.": " : Boostnote 的開發組態", " : Development configurations for Boostnote.": " : Boostnote 的開發設定",
"Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO",
"License: GPL v3": "License: GPL v3", "License: GPL v3": "License: GPL v3",
"Analytics": "分析", "Analytics": "分析",
"Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote 收集匿名資料單純只為了提升軟體使用體驗,絕對不收集任何個人資料(包括筆記內容)", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote 收集匿名資料單純只為了提升軟體使用體驗,絕對不收集任何個人資料(包括筆記內容)",
"You can see how it works on ": "可以看看它的程式碼是如何運作 ", "You can see how it works on ": "可以看看它的程式碼是如何運作 ",
"You can choose to enable or disable this option.": "可以選擇啟用或停用這項功能", "You can choose to enable or disable this option.": "可以選擇啟用或停用這項功能",
"Enable analytics to help improve Boostnote": "允許數據分析以協助我們改進 Boostnote", "Enable analytics to help improve Boostnote": "啟用數據分析以協助我們改進 Boostnote",
"Crowdfunding": "群眾募資", "Crowdfunding": "群眾募資",
"Dear Boostnote users,": "親愛的用戶", "Dear Boostnote users,": "親愛的使用者",
"Thank you for using Boostnote!": "謝謝你使用 Boostnote", "Thank you for using Boostnote!": "感謝您使用 Boostnote",
"Boostnote is used in about 200 different countries and regions by an awesome community of developers.": "大約有 200 個不同的國家和地區的優秀開發者們都在使用 Boostnote", "Boostnote is used in about 200 different countries and regions by an awesome community of developers.": "大約有 200 個不同的國家和地區的優秀開發者們都在使用 Boostnote",
"To support our growing userbase, and satisfy community expectations,": "為了繼續支持這種發展,和滿足社群期待,", "To support our growing userbase, and satisfy community expectations,": "為了繼續支持我們的使用者成長與滿足社群期待,",
"we would like to invest more time and resources in this project.": "我們非常願意投入更多的時間和資源到這個專案中。", "we would like to invest more time and resources in this project.": "我們非常願意投入更多的時間和資源到這個專案中。",
"If you use Boostnote and see its potential, help us out by supporting the project on OpenCollective!": "如果喜歡這款軟體並且看好它的潛力, 請在 OpenCollective 上支持我們!", "If you use Boostnote and see its potential, help us out by supporting the project on OpenCollective!": "如果喜歡這款軟體並且看好它的潛力, 請在 OpenCollective 上支持我們!",
"Thanks,": "十分感謝!", "Thanks,": "十分感謝!",
"The Boostnote Team": "Boostnote 的維護人員", "The Boostnote Team": "Boostnote 的團隊",
"Support via OpenCollective": "在 OpenCollective 上支持我們", "Support via OpenCollective": "在 OpenCollective 上支持我們",
"Language": "語言", "Language": "語言",
"Default New Note": "新筆記預設類型",
"English": "English", "English": "English",
"German": "German", "German": "德文",
"French": "French", "French": "法文",
"Show \"Saved to Clipboard\" notification when copying": "複製的時候,顯示 \"已複製到剪貼簿\" 的通知", "Show \"Saved to Clipboard\" notification when copying": "複製的時候,顯示 \"已複製到剪貼簿\" 的通知",
"All Notes": "所有筆記", "All Notes": "所有筆記",
"Starred": "星號收藏", "Starred": "我的最愛",
"Are you sure to ": "確定要 ", "Are you sure to ": "確定要 ",
" delete": " 刪除", " delete": " 刪除",
"this folder?": "這個資料夾嗎?", "this folder?": "這個資料夾嗎?",
"Confirm": "確認", "Confirm": "確認",
@@ -113,13 +122,14 @@
"Updated": "依更新時間排序", "Updated": "依更新時間排序",
"Created": "依建立時間排序", "Created": "依建立時間排序",
"Alphabetically": "依字母排序", "Alphabetically": "依字母排序",
"Counter": "計數器",
"Default View": "預設顯示", "Default View": "預設顯示",
"Compressed View": "緊密顯示", "Compressed View": "緊密顯示",
"Search": "搜尋", "Search": "搜尋",
"Blog Type": "部落格類型", "Blog Type": "部落格類型",
"Blog Address": "部落格網址", "Blog Address": "部落格網址",
"Save": "儲存", "Save": "儲存",
"Auth": "證", "Auth": "證",
"Authentication Method": "認證方法", "Authentication Method": "認證方法",
"JWT": "JWT", "JWT": "JWT",
"USER": "USER", "USER": "USER",
@@ -127,47 +137,61 @@
"Storage": "儲存空間", "Storage": "儲存空間",
"Hotkeys": "快捷鍵", "Hotkeys": "快捷鍵",
"Show/Hide Boostnote": "顯示/隱藏 Boostnote", "Show/Hide Boostnote": "顯示/隱藏 Boostnote",
"Toggle Editor Mode": "切換編輯器模式",
"Delete Note": "刪除模式",
"Restore": "還原", "Restore": "還原",
"Permanent Delete": "永久刪除", "Permanent Delete": "永久刪除",
"Confirm note deletion": "確認刪除筆記", "Confirm note deletion": "確認刪除筆記",
"This will permanently remove this note.": "這將會永久地刪除這條筆記", "This will permanently remove this note.": "永久地刪除筆記",
"Successfully applied!": "設定成功", "Successfully applied!": "設定成功",
"Albanian": "Albanian", "Albanian": "阿爾巴尼亞語",
"Chinese (zh-CN)": "简体中文", "Chinese (zh-CN)": "简体中文 (zh-CN)",
"Chinese (zh-TW)": "繁體中文", "Chinese (zh-TW)": "繁體中文 (zh-TW)",
"Czech": "捷克文", "Czech": "捷克文",
"Danish": "Danish", "Danish": "丹麥文",
"Japanese": "Japanese", "Japanese": "日文",
"Korean": "Korean", "Korean": "韓文",
"Norwegian": "Norwegian", "Norwegian": "挪威語",
"Polish": "Polish", "Polish": "波蘭文",
"Portuguese": "Portuguese", "Portuguese": "葡萄牙文",
"Spanish": "Spanish", "Spanish": "西班牙文",
"Unsaved Changes!": "必須儲存一下!", "Unsaved Changes!": "必須儲存一下!",
"Russian": "Russian", "UserName": "使用者名稱",
"Password": "密碼",
"Russian": "俄羅斯語",
"Hungarian": "匈牙利語",
"Thai": "泰文 (ภาษาไทย)",
"Command(⌘)": "指令(⌘)",
"Add Storage": "新增儲存空間",
"Name": "名稱",
"Type": "類型",
"File System": "檔案系統",
"Setting up 3rd-party cloud storage integration:": "第三方雲端儲存空間設定:",
"Cloud-Syncing-and-Backup": "雲端同步與備份",
"Location": "位置",
"Add": "新增",
"Unlink Storage": "解除儲存空間連結",
"Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.": "從 Boostnote 移除解此連結. 資料並不會被刪除,如需刪除,請手動從硬碟資料夾中刪除資料。",
"Editor Rulers": "編輯器中顯示垂直尺規", "Editor Rulers": "編輯器中顯示垂直尺規",
"Enable": "啟用", "Enable": "啟用",
"Disable": "停用", "Disable": "停用",
"Sanitization": "過濾 HTML 程式碼", "Sanitization": "過濾 HTML 程式碼",
"Only allow secure html tags (recommended)": "只允許安全的 HTML 標籤 (建議)", "Only allow secure html tags (recommended)": "只允許安全的 HTML 標籤 (建議)",
"Render newlines in Markdown paragraphs as <br>": "在 Markdown 段落中使用 <br> 換行",
"Allow styles": "允許樣式", "Allow styles": "允許樣式",
"Allow dangerous html tags": "允許危險的 HTML 標籤", "Allow dangerous html tags": "允許危險的 HTML 標籤",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "將文本箭頭轉換為完整符號。 ⚠ 注意這會影響 Markdown 的 HTML 注釋。", "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "將文本箭頭轉換為完整符號。 ⚠ 注意這會影響 Markdown 的 HTML 注釋。",
"Default New Note": "預設新筆記類型", "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ 您貼上了一個不存在本筆記儲存空間的附加檔案連結。貼上附加檔案連結功能只支援剪下貼上於相同儲存空間之間。請改以拖拉 Drag&Drop 附加檔案!⚠",
"Show only related tags": "只顯示相關標籤", "Spellcheck disabled": "拼寫檢查已關閉",
"Snippet Default Language": "程式碼片段預設語言", "Save tags of a note in alphabetical order": "依照字母排序儲存標籤",
"Disable Direct Write (It will be applied after restarting)": "停用直接編輯 (重啟後生效)", "Enable live count of notes": "啟用即時統計筆記數量",
"Enable smart table editor": "啟用智能表格編輯器", "Enable smart table editor": "啟用智能表格編輯器",
"Enable smart quotes": "啟用智能引號", "Snippet Default Language": "程式碼片段預設語言",
"Allow line through checkbox": "替標示為完成的選框添加刪除線", "New notes are tagged with the filtering tags": "以過慮標籤標記新筆記",
"Custom CSS": "自定義 CSS", "Show menu bar": "顯示功能列",
"Allow custom CSS for preview": "允許預覽自定義 CSS", "Auto Detect": "自動偵測",
"Render newlines in Markdown paragraphs as <br>": "在 Markdown 段落中使用 <br> 換行", "Filter tags/folders...": "過濾標籤/資料夾...",
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠", "Enable HTML label in mermaid flowcharts": "在 mermaid 流程圖中啟用 HTML 標籤 ⚠ 本選項有潛在的 XSS 安全風險。",
"Spellcheck disabled": "Spellcheck disabled", "Wrap line in Snippet Note": "Snippet Note 行尾換行",
"Show menu bar": "Show menu bar", "Enable Auto Update": "Enable Auto Update"
"Auto Detect": "Auto Detect",
"Filter tags/folders...": "filter tags/folders...",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "boost", "name": "boost",
"productName": "Boostnote", "productName": "Boostnote",
"version": "0.15.0", "version": "0.16.1",
"main": "index.js", "main": "index.js",
"description": "Boostnote", "description": "Boostnote",
"license": "GPL-3.0", "license": "GPL-3.0",
@@ -61,6 +61,7 @@
"chart.js": "^2.7.2", "chart.js": "^2.7.2",
"codemirror": "^5.40.2", "codemirror": "^5.40.2",
"codemirror-mode-elixir": "^1.1.1", "codemirror-mode-elixir": "^1.1.1",
"command-exists": "^1.2.9",
"connected-react-router": "^6.4.0", "connected-react-router": "^6.4.0",
"electron-config": "^1.0.0", "electron-config": "^1.0.0",
"electron-gh-releases": "^2.0.4", "electron-gh-releases": "^2.0.4",
@@ -79,7 +80,7 @@
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"jsonlint-mod": "^1.7.4", "jsonlint-mod": "^1.7.4",
"katex": "^0.10.1", "katex": "^0.10.1",
"lodash": "^4.17.13", "lodash": "^4.17.19",
"lodash-move": "^1.1.1", "lodash-move": "^1.1.1",
"markdown-it": "^6.0.1", "markdown-it": "^6.0.1",
"markdown-it-abbr": "^1.0.4", "markdown-it-abbr": "^1.0.4",
@@ -95,7 +96,7 @@
"markdown-it-sup": "^1.0.0", "markdown-it-sup": "^1.0.0",
"markdown-toc": "^1.2.0", "markdown-toc": "^1.2.0",
"mdurl": "^1.0.1", "mdurl": "^1.0.1",
"mermaid": "^8.4.2", "mermaid": "^8.5.2",
"moment": "^2.10.3", "moment": "^2.10.3",
"mousetrap": "^1.6.2", "mousetrap": "^1.6.2",
"mousetrap-global-bind": "^1.1.0", "mousetrap-global-bind": "^1.1.0",

View File

@@ -1,6 +1,5 @@
{ {
"trailingComma": "es5", "singleQuote": true,
"tabWidth": 2,
"semi": false, "semi": false,
"singleQuote": true "jsxSingleQuote": true
} }

View File

@@ -1,4 +1,10 @@
> [We've launched desktop app of the new Boost Note now. We'll release its mobile app too in January 2020.](https://github.com/BoostIO/BoostNote.next) > [We've launched desktop and mobile app of the new Boost Note now.](https://github.com/BoostIO/BoostNote.next)
> ### [Boost Note for Teams](https://boosthub.io/)
>
> We've developed a collaborative workspace app called "Boost Hub" for developer teams.
>
> It's customizable and easy to optimize for your team like rego blocks and even lets you edit documents together in real-time!
![Boostnote app screenshot](./resources/repository/top.png) ![Boostnote app screenshot](./resources/repository/top.png)
@@ -15,6 +21,10 @@
[Find the latest release of Boostnote here!](https://github.com/BoostIO/boost-releases/releases/) [Find the latest release of Boostnote here!](https://github.com/BoostIO/boost-releases/releases/)
## Roadmap
[Boost Note Roadmap 2020](https://medium.com/boostnote/boost-note-roadmap-2020-9f06a642f5f1)
## Authors & Maintainers ## Authors & Maintainers
- [Rokt33r](https://github.com/rokt33r) - [Rokt33r](https://github.com/rokt33r)
@@ -22,11 +32,13 @@
- [ZeroX-DG](https://github.com/ZeroX-DG) - [ZeroX-DG](https://github.com/ZeroX-DG)
## Contributors ## Contributors
Thank you to all the people who have contributed to Boostnote! Thank you to all the people who have contributed to Boostnote!
<a href="https://github.com/BoostIO/Boostnote/graphs/contributors"><img src="https://opencollective.com/boostnoteio/contributors.svg?width=890" /></a> <a href="https://github.com/BoostIO/Boostnote/graphs/contributors"><img src="https://opencollective.com/boostnoteio/contributors.svg?width=890" /></a>
## Supporting Boostnote ## Supporting Boostnote
Boostnote is an open source project. It's an independent project with its ongoing development made possible thanks to the support by our amazing backers. Boostnote is an open source project. It's an independent project with its ongoing development made possible thanks to the support by our amazing backers.
Issues on Boostnote can be funded by anyone and the money will be distributed to contributors and maintainers. If you use Boostnote please consider becoming a backer: Issues on Boostnote can be funded by anyone and the money will be distributed to contributors and maintainers. If you use Boostnote please consider becoming a backer:
@@ -34,18 +46,22 @@ Issues on Boostnote can be funded by anyone and the money will be distributed to
[![Let's fund issues in this repository](https://issuehunt.io/static/embed/issuehunt-button-v1.svg)](https://issuehunt.io/repos/53266139) [![Let's fund issues in this repository](https://issuehunt.io/static/embed/issuehunt-button-v1.svg)](https://issuehunt.io/repos/53266139)
## Community ## Community
- [Facebook Group](https://www.facebook.com/groups/boostnote/) - [Facebook Group](https://www.facebook.com/groups/boostnote/)
- [Twitter](https://twitter.com/boostnoteapp) - [Twitter](https://twitter.com/boostnoteapp)
- [Slack Group](https://join.slack.com/t/boostnote-group/shared_invite/enQtMzkxOTk4ODkyNzc0LWQxZTQwNjBlMDI4YjkyYjg2MTRiZGJhNzA1YjQ5ODA5M2M0M2NlMjI5YjhiYWQzNzgzYmU0MDMwOTlmZmZmMGE) - [Slack Group](https://join.slack.com/t/boostnote-group/shared_invite/zt-cun7pas3-WwkaezxHBB1lCbUHrwQLXw)
- [Blog](https://medium.com/boostnote) - [Blog](https://medium.com/boostnote)
- [Reddit](https://www.reddit.com/r/Boostnote/) - [Reddit](https://www.reddit.com/r/Boostnote/)
### Boostnote mobile
A community project developing a mobile cross-platform version of boostnote for iOS and Android can be found here: [NoteApp](https://github.com/T0M0F/NoteApp)
#### More Information #### More Information
* Website: https://boostnote.io
* [Development](https://github.com/BoostIO/Boostnote/blob/master/docs/build.md): Development configurations for Boostnote.
* Copyright (C) 2016 - 2020 BoostIO, Inc.
- Website: https://boostnote.io
- [Development](https://github.com/BoostIO/Boostnote/blob/master/docs/build.md): Development configurations for Boostnote.
- Copyright (C) 2016 - 2020 BoostIO, Inc.
#### License #### License

View File

@@ -675,6 +675,109 @@ it('should remove the all ":storage" and noteKey references', function() {
' </p>\n' + ' </p>\n' +
' </body>\n' + ' </body>\n' +
'</html>' '</html>'
const expectedOutput =
'<html>\n' +
' <head>\n' +
' //header\n' +
' </head>\n' +
' <body data-theme="default">\n' +
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
' <p data-line="2">\n' +
' <img src="' +
storageFolder +
path.posix.sep +
'0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
' </p>\n' +
' <p data-line="4">\n' +
' <a href="' +
storageFolder +
path.posix.sep +
'0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
' </p>\n' +
' <p data-line="6">\n' +
' <img src="' +
storageFolder +
path.posix.sep +
'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
' </p>\n' +
' </body>\n' +
'</html>'
const actual = systemUnderTest.replaceStorageReferences(
testInput,
noteKey,
systemUnderTest.DESTINATION_FOLDER
)
expect(actual).toEqual(expectedOutput)
})
it('should make sure that "replaceStorageReferences" works with markdown content as well', function() {
const noteKey = 'noteKey'
const testInput =
'Test input' +
'![imageName](' +
systemUnderTest.STORAGE_FOLDER_PLACEHOLDER +
path.win32.sep +
noteKey +
path.win32.sep +
'image.jpg) \n' +
'[pdf](' +
systemUnderTest.STORAGE_FOLDER_PLACEHOLDER +
path.posix.sep +
noteKey +
path.posix.sep +
'pdf.pdf)'
const expectedOutput =
'Test input' +
'![imageName](' +
systemUnderTest.DESTINATION_FOLDER +
path.posix.sep +
'image.jpg) \n' +
'[pdf](' +
systemUnderTest.DESTINATION_FOLDER +
path.posix.sep +
'pdf.pdf)'
const actual = systemUnderTest.replaceStorageReferences(
testInput,
noteKey,
systemUnderTest.DESTINATION_FOLDER
)
expect(actual).toEqual(expectedOutput)
})
it('should replace the all ":storage" references', function() {
const storageFolder = systemUnderTest.DESTINATION_FOLDER
const noteKey = 'noteKey'
const testInput =
'<html>\n' +
' <head>\n' +
' //header\n' +
' </head>\n' +
' <body data-theme="default">\n' +
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
' <p data-line="2">\n' +
' <img src=":storage' +
mdurl.encode(path.sep) +
noteKey +
mdurl.encode(path.sep) +
'0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
' </p>\n' +
' <p data-line="4">\n' +
' <a href=":storage' +
mdurl.encode(path.sep) +
noteKey +
mdurl.encode(path.sep) +
'0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
' </p>\n' +
' <p data-line="6">\n' +
' <img src=":storage' +
mdurl.encode(path.sep) +
noteKey +
mdurl.encode(path.sep) +
'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
' </p>\n' +
' </body>\n' +
'</html>'
const expectedOutput = const expectedOutput =
'<html>\n' + '<html>\n' +
' <head>\n' + ' <head>\n' +
@@ -702,43 +805,45 @@ it('should remove the all ":storage" and noteKey references', function() {
' </p>\n' + ' </p>\n' +
' </body>\n' + ' </body>\n' +
'</html>' '</html>'
const actual = systemUnderTest.removeStorageAndNoteReferences( const actual = systemUnderTest.replaceStorageReferences(
testInput, testInput,
noteKey noteKey,
systemUnderTest.DESTINATION_FOLDER
) )
expect(actual).toEqual(expectedOutput) expect(actual).toEqual(expectedOutput)
}) })
it('should make sure that "removeStorageAndNoteReferences" works with markdown content as well', function() { it('should make sure that "replaceStorageReferences" works with markdown content as well', function() {
const noteKey = 'noteKey' const noteKey = 'noteKey'
const testInput = const testInput =
'Test input' + 'Test input' +
'![' + '![imageName](' +
systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER +
path.win32.sep + path.win32.sep +
noteKey + noteKey +
path.win32.sep + path.win32.sep +
'image.jpg](imageName}) \n' + 'image.jpg) \n' +
'[' + '[pdf](' +
systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER +
path.posix.sep + path.posix.sep +
noteKey + noteKey +
path.posix.sep + path.posix.sep +
'pdf.pdf](pdf})' 'pdf.pdf)'
const expectedOutput = const expectedOutput =
'Test input' + 'Test input' +
'![' + '![imageName](' +
systemUnderTest.DESTINATION_FOLDER + systemUnderTest.DESTINATION_FOLDER +
path.sep + path.posix.sep +
'image.jpg](imageName}) \n' + 'image.jpg) \n' +
'[' + '[pdf](' +
systemUnderTest.DESTINATION_FOLDER + systemUnderTest.DESTINATION_FOLDER +
path.sep + path.posix.sep +
'pdf.pdf](pdf})' 'pdf.pdf)'
const actual = systemUnderTest.removeStorageAndNoteReferences( const actual = systemUnderTest.replaceStorageReferences(
testInput, testInput,
noteKey noteKey,
systemUnderTest.DESTINATION_FOLDER
) )
expect(actual).toEqual(expectedOutput) expect(actual).toEqual(expectedOutput)
}) })
@@ -912,6 +1017,19 @@ it('should test that getAttachmentsPathAndStatus return null if noteKey, storage
expect(result).toBeNull() expect(result).toBeNull()
}) })
it('should test that getAttachmentsPathAndStatus return null if no storage found', function() {
const noteKey = 'test'
const storageKey = 'not_exist'
const markdownContent = ''
const result = systemUnderTest.getAttachmentsPathAndStatus(
markdownContent,
storageKey,
noteKey
)
expect(result).toBeNull()
})
it('should test that getAttachmentsPathAndStatus return the correct path and status for attachments', async function() { it('should test that getAttachmentsPathAndStatus return the correct path and status for attachments', async function() {
const dummyStorage = { path: 'dummyStoragePath' } const dummyStorage = { path: 'dummyStoragePath' }
const noteKey = 'noteKey' const noteKey = 'noteKey'

View File

@@ -1,4 +1,3 @@
const test = require('ava')
const copyFile = require('browser/main/lib/dataApi/copyFile') const copyFile = require('browser/main/lib/dataApi/copyFile')
const path = require('path') const path = require('path')
@@ -13,23 +12,25 @@ const srcPath = path.join(srcFolder, testFile)
const dstFolder = path.join(__dirname, '😇') const dstFolder = path.join(__dirname, '😇')
const dstPath = path.join(dstFolder, testFile) const dstPath = path.join(dstFolder, testFile)
test.before(t => { beforeAll(() => {
if (!fs.existsSync(srcFolder)) fs.mkdirSync(srcFolder) if (!fs.existsSync(srcFolder)) fs.mkdirSync(srcFolder)
fs.writeFileSync(srcPath, 'test') fs.writeFileSync(srcPath, 'test')
}) })
test('`copyFile` should handle encoded URI on src path', t => { it('`copyFile` should handle encoded URI on src path', done => {
return copyFile(encodeURI(srcPath), dstPath) return copyFile(encodeURI(srcPath), dstPath)
.then(() => { .then(() => {
t.true(true) expect(true).toBe(true)
done()
}) })
.catch(() => { .catch(() => {
t.true(false) expect(false).toBe(true)
done()
}) })
}) })
test.after(t => { afterAll(() => {
fs.unlinkSync(srcPath) fs.unlinkSync(srcPath)
fs.unlinkSync(dstPath) fs.unlinkSync(dstPath)
execSync(removeDirCommand + '"' + srcFolder + '"') execSync(removeDirCommand + '"' + srcFolder + '"')

View File

@@ -1,4 +1,3 @@
const test = require('ava')
const createFolder = require('browser/main/lib/dataApi/createFolder') const createFolder = require('browser/main/lib/dataApi/createFolder')
global.document = require('jsdom').jsdom('<body></body>') global.document = require('jsdom').jsdom('<body></body>')
@@ -19,32 +18,34 @@ const CSON = require('@rokt33r/season')
const storagePath = path.join(os.tmpdir(), 'test/create-folder') const storagePath = path.join(os.tmpdir(), 'test/create-folder')
test.beforeEach(t => { let storageContext
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache])) beforeAll(() => {
storageContext = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([storageContext.cache]))
}) })
test.serial('Create a folder', t => { it('Create a folder', done => {
const storageKey = t.context.storage.cache.key const storageKey = storageContext.cache.key
const input = { const input = {
name: 'created', name: 'created',
color: '#ff5555' color: '#ff5555'
} }
return Promise.resolve() return Promise.resolve()
.then(function doTest() { .then(() => {
return createFolder(storageKey, input) return createFolder(storageKey, input)
}) })
.then(function assert(data) { .then(data => {
t.true(_.find(data.storage.folders, input) != null) expect(_.find(data.storage.folders, input)).not.toBeNull()
const jsonData = CSON.readFileSync( const jsonData = CSON.readFileSync(
path.join(data.storage.path, 'boostnote.json') path.join(data.storage.path, 'boostnote.json')
) )
console.log(path.join(data.storage.path, 'boostnote.json')) expect(_.find(jsonData.folders, input)).not.toBeNull()
t.true(_.find(jsonData.folders, input) != null) done()
}) })
}) })
test.after(function after() { afterAll(() => {
localStorage.clear() localStorage.clear()
sander.rimrafSync(storagePath) sander.rimrafSync(storagePath)
}) })

View File

@@ -1,4 +1,3 @@
const test = require('ava')
const createNote = require('browser/main/lib/dataApi/createNote') const createNote = require('browser/main/lib/dataApi/createNote')
global.document = require('jsdom').jsdom('<body></body>') global.document = require('jsdom').jsdom('<body></body>')
@@ -19,14 +18,16 @@ const faker = require('faker')
const storagePath = path.join(os.tmpdir(), 'test/create-note') const storagePath = path.join(os.tmpdir(), 'test/create-note')
test.beforeEach(t => { let storageContext
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache])) beforeEach(() => {
storageContext = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([storageContext.cache]))
}) })
test.serial('Create a note', t => { it('Create a note', done => {
const storageKey = t.context.storage.cache.key const storageKey = storageContext.cache.key
const folderKey = t.context.storage.json.folders[0].key const folderKey = storageContext.json.folders[0].key
const randLinesHighlightedArray = new Array(10) const randLinesHighlightedArray = new Array(10)
.fill() .fill()
@@ -58,58 +59,58 @@ test.serial('Create a note', t => {
input2.title = input2.content.split('\n').shift() input2.title = input2.content.split('\n').shift()
return Promise.resolve() return Promise.resolve()
.then(function doTest() { .then(() => {
return Promise.all([ return Promise.all([
createNote(storageKey, input1), createNote(storageKey, input1),
createNote(storageKey, input2) createNote(storageKey, input2)
]) ])
}) })
.then(function assert(data) { .then(data => {
const data1 = data[0] const data1 = data[0]
const data2 = data[1] const data2 = data[1]
t.is(storageKey, data1.storage) expect(storageKey).toEqual(data1.storage)
const jsonData1 = CSON.readFileSync( const jsonData1 = CSON.readFileSync(
path.join(storagePath, 'notes', data1.key + '.cson') path.join(storagePath, 'notes', data1.key + '.cson')
) )
t.is(input1.title, data1.title) expect(input1.title).toEqual(data1.title)
t.is(input1.title, jsonData1.title) expect(input1.title).toEqual(jsonData1.title)
t.is(input1.description, data1.description) expect(input1.description).toEqual(data1.description)
t.is(input1.description, jsonData1.description) expect(input1.description).toEqual(jsonData1.description)
t.is(input1.tags.length, data1.tags.length) expect(input1.tags.length).toEqual(data1.tags.length)
t.is(input1.tags.length, jsonData1.tags.length) expect(input1.tags.length).toEqual(jsonData1.tags.length)
t.is(input1.snippets.length, data1.snippets.length) expect(input1.snippets.length).toEqual(data1.snippets.length)
t.is(input1.snippets.length, jsonData1.snippets.length) expect(input1.snippets.length).toEqual(jsonData1.snippets.length)
t.is(input1.snippets[0].content, data1.snippets[0].content) expect(input1.snippets[0].content).toEqual(data1.snippets[0].content)
t.is(input1.snippets[0].content, jsonData1.snippets[0].content) expect(input1.snippets[0].content).toEqual(jsonData1.snippets[0].content)
t.is(input1.snippets[0].name, data1.snippets[0].name) expect(input1.snippets[0].name).toEqual(data1.snippets[0].name)
t.is(input1.snippets[0].name, jsonData1.snippets[0].name) expect(input1.snippets[0].name).toEqual(jsonData1.snippets[0].name)
t.deepEqual( expect(input1.snippets[0].linesHighlighted).toEqual(
input1.snippets[0].linesHighlighted,
data1.snippets[0].linesHighlighted data1.snippets[0].linesHighlighted
) )
t.deepEqual( expect(input1.snippets[0].linesHighlighted).toEqual(
input1.snippets[0].linesHighlighted,
jsonData1.snippets[0].linesHighlighted jsonData1.snippets[0].linesHighlighted
) )
t.is(storageKey, data2.storage) expect(storageKey).toEqual(data2.storage)
const jsonData2 = CSON.readFileSync( const jsonData2 = CSON.readFileSync(
path.join(storagePath, 'notes', data2.key + '.cson') path.join(storagePath, 'notes', data2.key + '.cson')
) )
t.is(input2.title, data2.title) expect(input2.title).toEqual(data2.title)
t.is(input2.title, jsonData2.title) expect(input2.title).toEqual(jsonData2.title)
t.is(input2.content, data2.content) expect(input2.content).toEqual(data2.content)
t.is(input2.content, jsonData2.content) expect(input2.content).toEqual(jsonData2.content)
t.is(input2.tags.length, data2.tags.length) expect(input2.tags.length).toEqual(data2.tags.length)
t.is(input2.tags.length, jsonData2.tags.length) expect(input2.tags.length).toEqual(jsonData2.tags.length)
t.deepEqual(input2.linesHighlighted, data2.linesHighlighted) expect(input2.linesHighlighted).toEqual(data2.linesHighlighted)
t.deepEqual(input2.linesHighlighted, jsonData2.linesHighlighted) expect(input2.linesHighlighted).toEqual(jsonData2.linesHighlighted)
done()
}) })
}) })
test.after(function after() { afterAll(function after() {
localStorage.clear() localStorage.clear()
sander.rimrafSync(storagePath) sander.rimrafSync(storagePath)
}) })

View File

@@ -1,4 +1,3 @@
const test = require('ava')
const createNoteFromUrl = require('browser/main/lib/dataApi/createNoteFromUrl') const createNoteFromUrl = require('browser/main/lib/dataApi/createNoteFromUrl')
global.document = require('jsdom').jsdom('<body></body>') global.document = require('jsdom').jsdom('<body></body>')
@@ -18,32 +17,34 @@ const CSON = require('@rokt33r/season')
const storagePath = path.join(os.tmpdir(), 'test/create-note-from-url') const storagePath = path.join(os.tmpdir(), 'test/create-note-from-url')
test.beforeEach(t => { let storageContext
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache])) beforeEach(() => {
storageContext = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([storageContext.cache]))
}) })
test.serial('Create a note from URL', t => { it('Create a note from URL', () => {
const storageKey = t.context.storage.cache.key const storageKey = storageContext.cache.key
const folderKey = t.context.storage.json.folders[0].key const folderKey = storageContext.json.folders[0].key
const url = 'https://shapeshed.com/writing-cross-platform-node/' const url = 'https://shapeshed.com/writing-cross-platform-node/'
return createNoteFromUrl(url, storageKey, folderKey).then(function assert({ return createNoteFromUrl(url, storageKey, folderKey).then(function assert({
note note
}) { }) {
t.is(storageKey, note.storage) expect(storageKey).toEqual(note.storage)
const jsonData = CSON.readFileSync( const jsonData = CSON.readFileSync(
path.join(storagePath, 'notes', note.key + '.cson') path.join(storagePath, 'notes', note.key + '.cson')
) )
// Test if saved content is matching the created in memory note // Test if saved content is matching the created in memory note
t.is(note.content, jsonData.content) expect(note.content).toEqual(jsonData.content)
t.is(note.tags.length, jsonData.tags.length) expect(note.tags.length).toEqual(jsonData.tags.length)
}) })
}) })
test.after(function after() { afterAll(function after() {
localStorage.clear() localStorage.clear()
sander.rimrafSync(storagePath) sander.rimrafSync(storagePath)
}) })

View File

@@ -1,4 +1,3 @@
const test = require('ava')
const createSnippet = require('browser/main/lib/dataApi/createSnippet') const createSnippet = require('browser/main/lib/dataApi/createSnippet')
const sander = require('sander') const sander = require('sander')
const os = require('os') const os = require('os')
@@ -7,29 +6,27 @@ const path = require('path')
const snippetFilePath = path.join(os.tmpdir(), 'test', 'create-snippet') const snippetFilePath = path.join(os.tmpdir(), 'test', 'create-snippet')
const snippetFile = path.join(snippetFilePath, 'snippets.json') const snippetFile = path.join(snippetFilePath, 'snippets.json')
test.beforeEach(t => { beforeEach(() => {
sander.writeFileSync(snippetFile, '[]') sander.writeFileSync(snippetFile, '[]')
}) })
test.serial('Create a snippet', t => { it('Create a snippet', () => {
return Promise.resolve() return Promise.resolve()
.then(function doTest() { .then(() => Promise.all([createSnippet(snippetFile)]))
return Promise.all([createSnippet(snippetFile)])
})
.then(function assert(data) { .then(function assert(data) {
data = data[0] data = data[0]
const snippets = JSON.parse(sander.readFileSync(snippetFile)) const snippets = JSON.parse(sander.readFileSync(snippetFile))
const snippet = snippets.find( const snippet = snippets.find(
currentSnippet => currentSnippet.id === data.id currentSnippet => currentSnippet.id === data.id
) )
t.not(snippet, undefined) expect(snippet).not.toBeUndefined()
t.is(snippet.name, data.name) expect(snippet.name).toEqual(data.name)
t.deepEqual(snippet.prefix, data.prefix) expect(snippet.prefix).toEqual(data.prefix)
t.is(snippet.content, data.content) expect(snippet.content).toEqual(data.content)
t.deepEqual(snippet.linesHighlighted, data.linesHighlighted) expect(snippet.linesHighlighted).toEqual(data.linesHighlighted)
}) })
}) })
test.after.always(() => { afterAll(() => {
sander.rimrafSync(snippetFilePath) sander.rimrafSync(snippetFilePath)
}) })

View File

@@ -1,4 +1,3 @@
const test = require('ava')
const deleteFolder = require('browser/main/lib/dataApi/deleteFolder') const deleteFolder = require('browser/main/lib/dataApi/deleteFolder')
const attachmentManagement = require('browser/main/lib/dataApi/attachmentManagement') const attachmentManagement = require('browser/main/lib/dataApi/attachmentManagement')
const createNote = require('browser/main/lib/dataApi/createNote') const createNote = require('browser/main/lib/dataApi/createNote')
@@ -23,14 +22,16 @@ const CSON = require('@rokt33r/season')
const storagePath = path.join(os.tmpdir(), 'test/delete-folder') const storagePath = path.join(os.tmpdir(), 'test/delete-folder')
test.beforeEach(t => { let storageContext
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache])) beforeEach(() => {
storageContext = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([storageContext.cache]))
}) })
test.serial('Delete a folder', t => { it('Delete a folder', () => {
const storageKey = t.context.storage.cache.key const storageKey = storageContext.cache.key
const folderKey = t.context.storage.json.folders[0].key const folderKey = storageContext.json.folders[0].key
let noteKey let noteKey
const input1 = { const input1 = {
@@ -72,16 +73,15 @@ test.serial('Delete a folder', t => {
return deleteFolder(storageKey, folderKey) return deleteFolder(storageKey, folderKey)
}) })
.then(function assert(data) { .then(function assert(data) {
t.true(_.find(data.storage.folders, { key: folderKey }) == null) expect(_.find(data.storage.folders, { key: folderKey })).toBeUndefined()
const jsonData = CSON.readFileSync( const jsonData = CSON.readFileSync(
path.join(data.storage.path, 'boostnote.json') path.join(data.storage.path, 'boostnote.json')
) )
t.true(_.find(jsonData.folders, { key: folderKey }) == null) expect(_.find(jsonData.folders, { key: folderKey })).toBeUndefined()
const notePaths = sander.readdirSync(data.storage.path, 'notes') const notePaths = sander.readdirSync(data.storage.path, 'notes')
t.is( expect(notePaths.length).toBe(
notePaths.length, storageContext.notes.filter(note => note.folder !== folderKey).length
t.context.storage.notes.filter(note => note.folder !== folderKey).length
) )
const attachmentFolderPath = path.join( const attachmentFolderPath = path.join(
@@ -89,11 +89,11 @@ test.serial('Delete a folder', t => {
attachmentManagement.DESTINATION_FOLDER, attachmentManagement.DESTINATION_FOLDER,
noteKey noteKey
) )
t.false(fs.existsSync(attachmentFolderPath)) expect(fs.existsSync(attachmentFolderPath)).toBe(false)
}) })
}) })
test.after.always(function after() { afterAll(() => {
localStorage.clear() localStorage.clear()
sander.rimrafSync(storagePath) sander.rimrafSync(storagePath)
}) })

View File

@@ -52,12 +52,20 @@ test.serial('Export a folder', t => {
} }
input2.title = 'input2' input2.title = 'input2'
const config = {
export: {
metadata: 'DONT_EXPORT',
variable: 'boostnote',
prefixAttachmentFolder: false
}
}
return createNote(storageKey, input1) return createNote(storageKey, input1)
.then(function() { .then(function() {
return createNote(storageKey, input2) return createNote(storageKey, input2)
}) })
.then(function() { .then(function() {
return exportFolder(storageKey, folderKey, 'md', storagePath) return exportFolder(storageKey, folderKey, 'md', storagePath, config)
}) })
.then(function assert() { .then(function assert() {
let filePath = path.join(storagePath, 'input1.md') let filePath = path.join(storagePath, 'input1.md')

View File

@@ -35,7 +35,16 @@ test.serial('Export a storage', t => {
acc[folder.key] = folder.name acc[folder.key] = folder.name
return acc return acc
}, {}) }, {})
return exportStorage(storageKey, 'md', exportDir).then(() => {
const config = {
export: {
metadata: 'DONT_EXPORT',
variable: 'boostnote',
prefixAttachmentFolder: false
}
}
return exportStorage(storageKey, 'md', exportDir, config).then(() => {
notes.forEach(note => { notes.forEach(note => {
const noteDir = path.join( const noteDir = path.join(
exportDir, exportDir,

View File

@@ -0,0 +1,189 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Markdown.render() should render PlantUML Ditaa correctly 1`] = `
"<img src=\\"http://www.plantuml.com/plantuml/png/SoWkIImgISaiIKpaqjQ50cq51GLj93Q2mrMZ00NQO3cmHX3RJW4cKmDI4v9QKQ805a8nfyObCp6zA34NgCObFxiqDpMl1AIcHj4tCJqpLH5i18evG52TKbk3B8og1kmC0cvMKB1Im0NYkA2ckMRcANWabgQbvYau5YMbPfP0p4UOWmcqkHnIyrB0GG00\\" alt=\\"uml diagram\\" />
"
`;
exports[`Markdown.render() should render PlantUML Gantt correctly 1`] = `
"<img src=\\"http://www.plantuml.com/plantuml/svg/SoWkIImgIK_CAodXYWueoY_9BwaiI5L8IItEJC-BLSX9B2ufLZ0qLKX9h2pcYWv9BIvHA82fWaiRu906crsia5YYW6cqUh52QbuAbmEG0DiE0000\\" alt=\\"uml diagram\\" />
"
`;
exports[`Markdown.render() should render PlantUML MindMaps correctly 1`] = `
"<img src=\\"http://www.plantuml.com/plantuml/svg/JOzD3e8m44Rtd6BMtNW192IM5I29HEDsAbKdeLD2MvNRIsjCMCsRlFd9LpgFipV4Wy4f4o2r8kHC23Yhm3wi9A0X3XzeYNrgwx1H6wvb1KTjqtRJoYhMtexBSAqJUescwoEUq4tn3xp9Fm7XfUS5HiiFO3Gw7SjT4QUCkkKxLy2-WAvl3rkrtEclBdOCXcnMwZN7ByiN\\" alt=\\"uml diagram\\" />
"
`;
exports[`Markdown.render() should render PlantUML Umls correctly 1`] = `
"<img src=\\"http://www.plantuml.com/plantuml/svg/LOzD2eCm44RtESMtj0jx01V5E_G4Gvngo2_912gbTsz4LBfylCV7p5Y4ibJlbEENG2AocHV1P39hCJ6eOar8bCaZaROqyrDMnzWqXTcn8YqnGzSYqNC-q76sweoW5zOsLi57uMpHz-WESslY0jmVw1AjdaE30IPeLoVUceLTslrL3-2tS9ZA_qZRtm_vgh7PzkOF\\" alt=\\"uml diagram\\" />
"
`;
exports[`Markdown.render() should render PlantUML WBS correctly 1`] = `
"<img src=\\"http://www.plantuml.com/plantuml/svg/ZP2_JiD03CRtFeNdRF04fR140gdGeREv-z8plVYYimFYxSabKbaxsR9-ylTdRyxLVpvjrz5XDb6OqR6MqEPRYSXPz4BdmsdNTVJAiuP4da1JBLy8lbmxUYxZbE6Wa_CLgUI8IXymS0rf9NeL5yxKDt24EhiKfMDcRNzVO79HcX8RLdvLfZBGa_KtFx2RKcpK7TZ3dTpZfWgskMAZ9jIXr94rW4PubM1RbBZOb-6NtcS9LpgBjlj_1w9QldbPjZHxQ5pg_GC0\\" alt=\\"uml diagram\\" />
"
`;
exports[`Markdown.render() should render footnote correctly 1`] = `
"<p data-line=\\"1\\"><sup class=\\"footnote-ref\\"><a href=\\"#fn1\\" id=\\"fnref1\\">[1]</a></sup><br />
hello-world: <a href=\\"https://github.com/BoostIO/Boostnote/\\">https://github.com/BoostIO/Boostnote/</a></p>
<hr class=\\"footnotes-sep\\" />
<section class=\\"footnotes\\">
<ol class=\\"footnotes-list\\">
<li id=\\"fn1\\" class=\\"footnote-item\\"><p>hello-world <a href=\\"#fnref1\\" class=\\"footnote-backref\\">↩︎</a></p>
</li>
</ol>
</section>
"
`;
exports[`Markdown.render() should render line breaks correctly 1`] = `
"<p data-line=\\"0\\">This is the first line.<br />
This is the second line.</p>
"
`;
exports[`Markdown.render() should render line breaks correctly 2`] = `
"<p data-line=\\"0\\">This is the first line.
This is the second line.</p>
"
`;
exports[`Markdown.render() should render shortcuts correctly 1`] = `
"<p data-line=\\"0\\"><kbd>Ctrl</kbd></p>
<p data-line=\\"2\\"><kbd>Ctrl</kbd></p>
"
`;
exports[`Markdown.render() should renders [TOC] placholder correctly 1`] = `
"<p data-line=\\"1\\"><div class=\\"markdownIt-TOC-wrapper\\"><ul class=\\"markdownIt-TOC\\">
<li><a href=\\"#H1\\">H1</a>
<ul>
<li><a href=\\"#H2\\">H2</a>
<ul>
<li><a href=\\"#H3\\">H3</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</div></p>
<h1 id=\\"H1\\" data-line=\\"2\\">H1</h1>
<h2 id=\\"H2\\" data-line=\\"3\\">H2</h2>
<h3 id=\\"H3\\" data-line=\\"4\\">H3</h3>
<p data-line=\\"5\\">###$ H4</p>
"
`;
exports[`Markdown.render() should renders KaTeX correctly 1`] = `
"<span class=\\"katex-display\\"><span class=\\"katex\\"><span class=\\"katex-mathml\\"><math><semantics><mrow><mi>c</mi><mo>=</mo><mi>p</mi><mi>m</mi><mi>s</mi><mi>q</mi><mi>r</mi><mi>t</mi><mrow><msup><mi>a</mi><mn>2</mn></msup><mo>+</mo><msup><mi>b</mi><mn>2</mn></msup></mrow></mrow><annotation encoding=\\"application/x-tex\\">c = pmsqrt{a^2 + b^2}</annotation></semantics></math></span><span class=\\"katex-html\\" aria-hidden=\\"true\\"><span class=\\"base\\"><span class=\\"strut\\" style=\\"height:0.43056em;vertical-align:0em;\\"></span><span class=\\"mord mathdefault\\">c</span><span class=\\"mspace\\" style=\\"margin-right:0.2777777777777778em;\\"></span><span class=\\"mrel\\">=</span><span class=\\"mspace\\" style=\\"margin-right:0.2777777777777778em;\\"></span></span><span class=\\"base\\"><span class=\\"strut\\" style=\\"height:1.0585479999999998em;vertical-align:-0.19444em;\\"></span><span class=\\"mord mathdefault\\">p</span><span class=\\"mord mathdefault\\">m</span><span class=\\"mord mathdefault\\">s</span><span class=\\"mord mathdefault\\" style=\\"margin-right:0.03588em;\\">q</span><span class=\\"mord mathdefault\\" style=\\"margin-right:0.02778em;\\">r</span><span class=\\"mord mathdefault\\">t</span><span class=\\"mord\\"><span class=\\"mord\\"><span class=\\"mord mathdefault\\">a</span><span class=\\"msupsub\\"><span class=\\"vlist-t\\"><span class=\\"vlist-r\\"><span class=\\"vlist\\" style=\\"height:0.8641079999999999em;\\"><span style=\\"top:-3.113em;margin-right:0.05em;\\"><span class=\\"pstrut\\" style=\\"height:2.7em;\\"></span><span class=\\"sizing reset-size6 size3 mtight\\"><span class=\\"mord mtight\\">2</span></span></span></span></span></span></span></span><span class=\\"mspace\\" style=\\"margin-right:0.2222222222222222em;\\"></span><span class=\\"mbin\\">+</span><span class=\\"mspace\\" style=\\"margin-right:0.2222222222222222em;\\"></span><span class=\\"mord\\"><span class=\\"mord mathdefault\\">b</span><span class=\\"msupsub\\"><span class=\\"vlist-t\\"><span class=\\"vlist-r\\"><span class=\\"vlist\\" style=\\"height:0.8641079999999999em;\\"><span style=\\"top:-3.113em;margin-right:0.05em;\\"><span class=\\"pstrut\\" style=\\"height:2.7em;\\"></span><span class=\\"sizing reset-size6 size3 mtight\\"><span class=\\"mord mtight\\">2</span></span></span></span></span></span></span></span></span></span></span></span></span>
"
`;
exports[`Markdown.render() should renders abbrevations correctly 1`] = `
"<h2 id=\\"abbr\\" data-line=\\"1\\">abbr</h2>
<p data-line=\\"3\\">The <abbr title=\\"Hyper Text Markup Language\\">HTML</abbr> specification<br />
is maintained by the <abbr title=\\"World Wide Web Consortium\\">W3C</abbr>.</p>
"
`;
exports[`Markdown.render() should renders checkboxes 1`] = `
"<ul>
<li class=\\"taskListItem\\" data-line=\\"1\\"><input type=\\"checkbox\\" id=\\"checkbox-2\\" /> Unchecked</li>
<li class=\\"taskListItem checked\\" data-line=\\"2\\"><input type=\\"checkbox\\" checked id=\\"checkbox-3\\" /> Checked</li>
</ul>
"
`;
exports[`Markdown.render() should renders codeblock correctly 1`] = `
"<pre class=\\"code CodeMirror\\" data-line=\\"1\\">
<span class=\\"filename\\">filename.js</span>
<span class=\\"lineNumber CodeMirror-gutters\\"><span class=\\"CodeMirror-linenumber\\">2</span></span>
<code class=\\"js\\">var project = 'boostnote';
</code>
</pre>"
`;
exports[`Markdown.render() should renders definition lists correctly 1`] = `
"<h2 id=\\"definition-list\\" data-line=\\"1\\">definition list</h2>
<h3 id=\\"list-1\\" data-line=\\"3\\">list 1</h3>
<dl>
<dt data-line=\\"5\\">Term 1</dt>
<dd data-line=\\"6\\">Definition 1</dd>
<dt data-line=\\"8\\">Term 2</dt>
<dd data-line=\\"9\\">Definition 2a</dd>
<dd data-line=\\"10\\">Definition 2b</dd>
</dl>
<p data-line=\\"12\\">Term 3<br />
~</p>
<h3 id=\\"list-2\\" data-line=\\"16\\">list 2</h3>
<dl>
<dt data-line=\\"18\\">Term 1</dt>
<dd data-line=\\"20\\">
<p data-line=\\"20\\">Definition 1</p>
</dd>
<dt data-line=\\"22\\">Term 2 with <em>inline markup</em></dt>
<dd data-line=\\"24\\">
<p data-line=\\"24\\">Definition 2</p>
<pre><code> { some code, part of Definition 2 }
</code></pre>
<p data-line=\\"28\\">Third paragraph of definition 2.</p>
</dd>
</dl>
"
`;
exports[`Markdown.render() should renders markdown correctly 1`] = `
"<h1 id=\\"Welcome-to-Boostnote\\" data-line=\\"1\\">Welcome to Boostnote!</h1>
<h2 id=\\"Click-here-to-edit-markdown\\" data-line=\\"2\\">Click here to edit markdown 👋</h2>
<iframe width=\\"560\\" height=\\"315\\" src=\\"https://www.youtube.com/embed/L0qNPLsvmyM\\" frameborder=\\"0\\" allowfullscreen></iframe>
<h2 id=\\"Docs\\" data-line=\\"6\\">Docs 📝</h2>
<ul>
<li data-line=\\"7\\"><a href=\\"https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe\\">Boostnote | Boost your happiness, productivity and creativity.</a></li>
<li data-line=\\"8\\"><a href=\\"https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup\\">Cloud Syncing &amp; Backups</a></li>
<li data-line=\\"9\\"><a href=\\"https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps\\">How to sync your data across Desktop and Mobile apps</a></li>
<li data-line=\\"10\\"><a href=\\"https://github.com/BoostIO/Boostnote/wiki/Evernote\\">Convert data from <strong>Evernote</strong> to Boostnote.</a></li>
<li data-line=\\"11\\"><a href=\\"https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts\\">Keyboard Shortcuts</a></li>
<li data-line=\\"12\\"><a href=\\"https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode\\">Keymaps in Editor mode</a></li>
<li data-line=\\"13\\"><a href=\\"https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting\\">How to set syntax highlight in Snippet note</a></li>
</ul>
<hr />
<h2 id=\\"Article-Archive\\" data-line=\\"17\\">Article Archive 📚</h2>
<ul>
<li data-line=\\"18\\"><a href=\\"http://bit.ly/2mOJPu7\\">Reddit English</a></li>
<li data-line=\\"19\\"><a href=\\"https://www.reddit.com/r/boostnote_es/\\">Reddit Spanish</a></li>
<li data-line=\\"20\\"><a href=\\"https://www.reddit.com/r/boostnote_cn/\\">Reddit Chinese</a></li>
<li data-line=\\"21\\"><a href=\\"https://www.reddit.com/r/boostnote_jp/\\">Reddit Japanese</a></li>
</ul>
<hr />
<h2 id=\\"Community\\" data-line=\\"25\\">Community 🍻</h2>
<ul>
<li data-line=\\"26\\"><a href=\\"http://bit.ly/2AWWzkD\\">GitHub</a></li>
<li data-line=\\"27\\"><a href=\\"http://bit.ly/2z8BUJZ\\">Twitter</a></li>
<li data-line=\\"28\\"><a href=\\"http://bit.ly/2jcca8t\\">Facebook Group</a></li>
</ul>
"
`;
exports[`Markdown.render() should renders sub correctly 1`] = `
"<h2 id=\\"sub\\" data-line=\\"1\\">sub</h2>
<p data-line=\\"3\\">H<sub>2</sub>0</p>
"
`;
exports[`Markdown.render() should renders sup correctly 1`] = `
"<h2 id=\\"sup\\" data-line=\\"1\\">sup</h2>
<p data-line=\\"3\\">29<sup>th</sup></p>
"
`;
exports[`Markdown.render() should text with quotes correctly 1`] = `
"<p data-line=\\"0\\">This is a “QUOTE”.</p>
"
`;
exports[`Markdown.render() should text with quotes correctly 2`] = `
"<p data-line=\\"0\\">This is a &quot;QUOTE&quot;.</p>
"
`;

View File

@@ -1,46 +1,45 @@
const { escapeHtmlCharacters } = require('browser/lib/utils') const { escapeHtmlCharacters } = require('browser/lib/utils')
const test = require('ava')
test('escapeHtmlCharacters should return the original string if nothing needed to escape', t => { test('escapeHtmlCharacters should return the original string if nothing needed to escape', () => {
const input = 'Nothing to be escaped' const input = 'Nothing to be escaped'
const expected = 'Nothing to be escaped' const expected = 'Nothing to be escaped'
const actual = escapeHtmlCharacters(input) const actual = escapeHtmlCharacters(input)
t.is(actual, expected) expect(actual).toBe(expected)
}) })
test('escapeHtmlCharacters should skip code block if that option is enabled', t => { test('escapeHtmlCharacters should skip code block if that option is enabled', () => {
const input = ` <no escape> const input = ` <no escape>
<escapeMe>` <escapeMe>`
const expected = ` <no escape> const expected = ` <no escape>
&lt;escapeMe&gt;` &lt;escapeMe&gt;`
const actual = escapeHtmlCharacters(input, { detectCodeBlock: true }) const actual = escapeHtmlCharacters(input, { detectCodeBlock: true })
t.is(actual, expected) expect(actual).toBe(expected)
}) })
test('escapeHtmlCharacters should NOT skip character not in code block but start with 4 spaces', t => { test('escapeHtmlCharacters should NOT skip character not in code block but start with 4 spaces', () => {
const input = '4 spaces &' const input = '4 spaces &'
const expected = '4 spaces &amp;' const expected = '4 spaces &amp;'
const actual = escapeHtmlCharacters(input, { detectCodeBlock: true }) const actual = escapeHtmlCharacters(input, { detectCodeBlock: true })
t.is(actual, expected) expect(actual).toBe(expected)
}) })
test('escapeHtmlCharacters should NOT skip code block if that option is NOT enabled', t => { test('escapeHtmlCharacters should NOT skip code block if that option is NOT enabled', () => {
const input = ` <no escape> const input = ` <no escape>
<escapeMe>` <escapeMe>`
const expected = ` &lt;no escape&gt; const expected = ` &lt;no escape&gt;
&lt;escapeMe&gt;` &lt;escapeMe&gt;`
const actual = escapeHtmlCharacters(input) const actual = escapeHtmlCharacters(input)
t.is(actual, expected) expect(actual).toBe(expected)
}) })
test("escapeHtmlCharacters should NOT escape & character if it's a part of an escaped character", t => { test("escapeHtmlCharacters should NOT escape & character if it's a part of an escaped character", () => {
const input = 'Do not escape &amp; or &quot; but do escape &' const input = 'Do not escape &amp; or &quot; but do escape &'
const expected = 'Do not escape &amp; or &quot; but do escape &amp;' const expected = 'Do not escape &amp; or &quot; but do escape &amp;'
const actual = escapeHtmlCharacters(input) const actual = escapeHtmlCharacters(input)
t.is(actual, expected) expect(actual).toBe(expected)
}) })
test('escapeHtmlCharacters should skip char if in code block', t => { test('escapeHtmlCharacters should skip char if in code block', () => {
const input = ` const input = `
\`\`\` \`\`\`
<dontescapeme> <dontescapeme>
@@ -62,12 +61,12 @@ dasdasdasd
\`\`\` \`\`\`
` `
const actual = escapeHtmlCharacters(input, { detectCodeBlock: true }) const actual = escapeHtmlCharacters(input, { detectCodeBlock: true })
t.is(actual, expected) expect(actual).toBe(expected)
}) })
test('escapeHtmlCharacters should return the correct result', t => { test('escapeHtmlCharacters should return the correct result', () => {
const input = '& < > " \'' const input = '& < > " \''
const expected = '&amp; &lt; &gt; &quot; &#39;' const expected = '&amp; &lt; &gt; &quot; &#39;'
const actual = escapeHtmlCharacters(input) const actual = escapeHtmlCharacters(input)
t.is(actual, expected) expect(actual).toBe(expected)
}) })

View File

@@ -1,4 +1,3 @@
const test = require('ava')
const { findStorage } = require('browser/lib/findStorage') const { findStorage } = require('browser/lib/findStorage')
global.document = require('jsdom').jsdom('<body></body>') global.document = require('jsdom').jsdom('<body></body>')
@@ -16,20 +15,22 @@ const sander = require('sander')
const os = require('os') const os = require('os')
const storagePath = path.join(os.tmpdir(), 'test/find-storage') const storagePath = path.join(os.tmpdir(), 'test/find-storage')
test.beforeEach(t => { let storageContext
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache])) beforeEach(() => {
storageContext = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([storageContext.cache]))
}) })
// Unit test // Unit test
test('findStorage() should return a correct storage path(string)', t => { test('findStorage() should return a correct storage path(string)', () => {
const storageKey = t.context.storage.cache.key const storageKey = storageContext.cache.key
t.is(findStorage(storageKey).key, storageKey) expect(findStorage(storageKey).key).toBe(storageKey)
t.is(findStorage(storageKey).path, storagePath) expect(findStorage(storageKey).path).toBe(storagePath)
}) })
test.after(function after() { afterAll(function after() {
localStorage.clear() localStorage.clear()
sander.rimrafSync(storagePath) sander.rimrafSync(storagePath)
}) })

View File

@@ -2,11 +2,10 @@
* @fileoverview Unit test for browser/lib/findTitle * @fileoverview Unit test for browser/lib/findTitle
*/ */
const test = require('ava')
const { findNoteTitle } = require('browser/lib/findNoteTitle') const { findNoteTitle } = require('browser/lib/findNoteTitle')
// Unit test // Unit test
test('findNoteTitle#find should return a correct title (string)', t => { test('findNoteTitle#find should return a correct title (string)', () => {
// [input, expected] // [input, expected]
const testCases = [ const testCases = [
['# hoge\nfuga', '# hoge'], ['# hoge\nfuga', '# hoge'],
@@ -20,15 +19,11 @@ test('findNoteTitle#find should return a correct title (string)', t => {
testCases.forEach(testCase => { testCases.forEach(testCase => {
const [input, expected] = testCase const [input, expected] = testCase
t.is( expect(findNoteTitle(input, false)).toBe(expected)
findNoteTitle(input, false),
expected,
`Test for find() input: ${input} expected: ${expected}`
)
}) })
}) })
test('findNoteTitle#find should ignore front matter when enableFrontMatterTitle=false', t => { test('findNoteTitle#find should ignore front matter when enableFrontMatterTitle=false', () => {
// [input, expected] // [input, expected]
const testCases = [ const testCases = [
['---\nlayout: test\ntitle: hoge hoge hoge \n---\n# fuga', '# fuga'], ['---\nlayout: test\ntitle: hoge hoge hoge \n---\n# fuga', '# fuga'],
@@ -38,15 +33,11 @@ test('findNoteTitle#find should ignore front matter when enableFrontMatterTitle
testCases.forEach(testCase => { testCases.forEach(testCase => {
const [input, expected] = testCase const [input, expected] = testCase
t.is( expect(findNoteTitle(input, false)).toBe(expected)
findNoteTitle(input, false),
expected,
`Test for find() input: ${input} expected: ${expected}`
)
}) })
}) })
test('findNoteTitle#find should respect front matter when enableFrontMatterTitle=true', t => { test('findNoteTitle#find should respect front matter when enableFrontMatterTitle=true', () => {
// [input, expected] // [input, expected]
const testCases = [ const testCases = [
[ [
@@ -59,15 +50,11 @@ test('findNoteTitle#find should respect front matter when enableFrontMatterTitl
testCases.forEach(testCase => { testCases.forEach(testCase => {
const [input, expected] = testCase const [input, expected] = testCase
t.is( expect(findNoteTitle(input, true)).toBe(expected)
findNoteTitle(input, true),
expected,
`Test for find() input: ${input} expected: ${expected}`
)
}) })
}) })
test('findNoteTitle#find should respect frontMatterTitleField when provided', t => { test('findNoteTitle#find should respect frontMatterTitleField when provided', () => {
// [input, expected] // [input, expected]
const testCases = [ const testCases = [
['---\ntitle: hoge\n---\n# fuga', '# fuga'], ['---\ntitle: hoge\n---\n# fuga', '# fuga'],
@@ -76,10 +63,6 @@ test('findNoteTitle#find should respect frontMatterTitleField when provided', t
testCases.forEach(testCase => { testCases.forEach(testCase => {
const [input, expected] = testCase const [input, expected] = testCase
t.is( expect(findNoteTitle(input, true, 'custom')).toBe(expected)
findNoteTitle(input, true, 'custom'),
expected,
`Test for find() input: ${input} expected: ${expected}`
)
}) })
}) })

View File

@@ -1,8 +1,7 @@
const test = require('ava')
const { getTodoStatus } = require('browser/lib/getTodoStatus') const { getTodoStatus } = require('browser/lib/getTodoStatus')
// Unit test // Unit test
test('getTodoStatus should return a correct hash object', t => { test('getTodoStatus should return a correct hash object', () => {
// [input, expected] // [input, expected]
const testCases = [ const testCases = [
['', { total: 0, completed: 0 }], ['', { total: 0, completed: 0 }],
@@ -40,15 +39,7 @@ test('getTodoStatus should return a correct hash object', t => {
testCases.forEach(testCase => { testCases.forEach(testCase => {
const [input, expected] = testCase const [input, expected] = testCase
t.is( expect(getTodoStatus(input).total).toBe(expected.total)
getTodoStatus(input).total, expect(getTodoStatus(input).completed).toBe(expected.completed)
expected.total,
`Test for getTodoStatus() input: ${input} expected: ${expected.total}`
)
t.is(
getTodoStatus(input).completed,
expected.completed,
`Test for getTodoStatus() input: ${input} expected: ${expected.completed}`
)
}) })
}) })

View File

@@ -1,11 +1,10 @@
/** /**
* @fileoverview Unit test for browser/lib/htmlTextHelper * @fileoverview Unit test for browser/lib/htmlTextHelper
*/ */
const test = require('ava')
const htmlTextHelper = require('browser/lib/htmlTextHelper') const htmlTextHelper = require('browser/lib/htmlTextHelper')
// Unit test // Unit test
test('htmlTextHelper#decodeEntities should return encoded text (string)', t => { test('htmlTextHelper#decodeEntities should return encoded text (string)', () => {
// [input, expected] // [input, expected]
const testCases = [ const testCases = [
['&lt;a href=', '<a href='], ['&lt;a href=', '<a href='],
@@ -21,15 +20,11 @@ test('htmlTextHelper#decodeEntities should return encoded text (string)', t => {
testCases.forEach(testCase => { testCases.forEach(testCase => {
const [input, expected] = testCase const [input, expected] = testCase
t.is( expect(htmlTextHelper.decodeEntities(input)).toBe(expected)
htmlTextHelper.decodeEntities(input),
expected,
`Test for decodeEntities() input: ${input} expected: ${expected}`
)
}) })
}) })
test('htmlTextHelper#decodeEntities() should return decoded text (string)', t => { test('htmlTextHelper#decodeEntities() should return decoded text (string)', () => {
// [input, expected] // [input, expected]
const testCases = [ const testCases = [
['<a href=', '&lt;a href='], ['<a href=', '&lt;a href='],
@@ -44,16 +39,12 @@ test('htmlTextHelper#decodeEntities() should return decoded text (string)', t =>
testCases.forEach(testCase => { testCases.forEach(testCase => {
const [input, expected] = testCase const [input, expected] = testCase
t.is( expect(htmlTextHelper.encodeEntities(input)).toBe(expected)
htmlTextHelper.encodeEntities(input),
expected,
`Test for encodeEntities() input: ${input} expected: ${expected}`
)
}) })
}) })
// Integration test // Integration test
test(t => { test(() => {
const testCases = [ const testCases = [
"var test = 'test'", "var test = 'test'",
"<a href='https://boostnote.io'>Boostnote", "<a href='https://boostnote.io'>Boostnote",
@@ -63,10 +54,6 @@ test(t => {
testCases.forEach(testCase => { testCases.forEach(testCase => {
const encodedText = htmlTextHelper.encodeEntities(testCase) const encodedText = htmlTextHelper.encodeEntities(testCase)
const decodedText = htmlTextHelper.decodeEntities(encodedText) const decodedText = htmlTextHelper.decodeEntities(encodedText)
t.is( expect(decodedText).toBe(testCase)
decodedText,
testCase,
'Integration test through encodedText() and decodedText()'
)
}) })
}) })

View File

@@ -1,10 +1,9 @@
/** /**
* @fileoverview Unit test for browser/lib/markdown * @fileoverview Unit test for browser/lib/markdown
*/ */
const test = require('ava')
const markdown = require('browser/lib/markdownTextHelper') const markdown = require('browser/lib/markdownTextHelper')
test(t => { test(() => {
// [input, expected] // [input, expected]
const testCases = [ const testCases = [
// List // List
@@ -42,10 +41,6 @@ test(t => {
testCases.forEach(testCase => { testCases.forEach(testCase => {
const [input, expected] = testCase const [input, expected] = testCase
t.is( expect(markdown.strip(input)).toBe(expected)
markdown.strip(input),
expected,
`Test for strip() input: ${input} expected: ${expected}`
)
}) })
}) })

View File

@@ -4,11 +4,10 @@
import CodeMirror from 'codemirror' import CodeMirror from 'codemirror'
require('codemirror/addon/search/searchcursor.js') require('codemirror/addon/search/searchcursor.js')
const test = require('ava')
const markdownToc = require('browser/lib/markdown-toc-generator') const markdownToc = require('browser/lib/markdown-toc-generator')
const EOL = require('os').EOL const EOL = require('os').EOL
test(t => { test(() => {
/** /**
* Contains array of test cases in format : * Contains array of test cases in format :
* [ * [
@@ -261,15 +260,11 @@ this is a text
const expectedToc = testCase[2].trim() const expectedToc = testCase[2].trim()
const generatedToc = markdownToc.generate(inputMd) const generatedToc = markdownToc.generate(inputMd)
t.is( expect(generatedToc).toBe(expectedToc)
generatedToc,
expectedToc,
`generate test : ${title} , generated : ${EOL}${generatedToc}, expected : ${EOL}${expectedToc}`
)
}) })
}) })
test(t => { test(() => {
/** /**
* Contains array of test cases in format : * Contains array of test cases in format :
* [ * [
@@ -667,10 +662,6 @@ this is a level one text
editor.setCursor(cursor) editor.setCursor(cursor)
markdownToc.generateInEditor(editor) markdownToc.generateInEditor(editor)
t.is( expect(expectedMd).toBe(editor.getValue())
expectedMd,
editor.getValue(),
`generateInEditor test : ${title} , generated : ${EOL}${editor.getValue()}, expected : ${EOL}${expectedMd}`
)
}) })
}) })

View File

@@ -1,4 +1,17 @@
import test from 'ava' jest.mock(
'electron',
() => {
return {
remote: {
app: {
getPath: jest.fn().mockReturnValue('.')
}
}
}
},
{ virtual: true }
)
import Markdown from 'browser/lib/markdown' import Markdown from 'browser/lib/markdown'
import markdownFixtures from '../fixtures/markdowns' import markdownFixtures from '../fixtures/markdowns'
@@ -6,100 +19,100 @@ import markdownFixtures from '../fixtures/markdowns'
// To test markdown options, initialize a new instance in your test case // To test markdown options, initialize a new instance in your test case
const md = new Markdown() const md = new Markdown()
test('Markdown.render() should renders markdown correctly', t => { test('Markdown.render() should renders markdown correctly', () => {
const rendered = md.render(markdownFixtures.basic) const rendered = md.render(markdownFixtures.basic)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })
test('Markdown.render() should renders codeblock correctly', t => { test('Markdown.render() should renders codeblock correctly', () => {
const rendered = md.render(markdownFixtures.codeblock) const rendered = md.render(markdownFixtures.codeblock)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })
test('Markdown.render() should renders KaTeX correctly', t => { test('Markdown.render() should renders KaTeX correctly', () => {
const rendered = md.render(markdownFixtures.katex) const rendered = md.render(markdownFixtures.katex)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })
test('Markdown.render() should renders checkboxes', t => { test('Markdown.render() should renders checkboxes', () => {
const rendered = md.render(markdownFixtures.checkboxes) const rendered = md.render(markdownFixtures.checkboxes)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })
test('Markdown.render() should text with quotes correctly', t => { test('Markdown.render() should text with quotes correctly', () => {
const renderedSmartQuotes = md.render(markdownFixtures.smartQuotes) const renderedSmartQuotes = md.render(markdownFixtures.smartQuotes)
t.snapshot(renderedSmartQuotes) expect(renderedSmartQuotes).toMatchSnapshot()
const newmd = new Markdown({ typographer: false }) const newmd = new Markdown({ typographer: false })
const renderedNonSmartQuotes = newmd.render(markdownFixtures.smartQuotes) const renderedNonSmartQuotes = newmd.render(markdownFixtures.smartQuotes)
t.snapshot(renderedNonSmartQuotes) expect(renderedNonSmartQuotes).toMatchSnapshot()
}) })
test('Markdown.render() should render line breaks correctly', t => { test('Markdown.render() should render line breaks correctly', () => {
const renderedBreaks = md.render(markdownFixtures.breaks) const renderedBreaks = md.render(markdownFixtures.breaks)
t.snapshot(renderedBreaks) expect(renderedBreaks).toMatchSnapshot()
const newmd = new Markdown({ breaks: false }) const newmd = new Markdown({ breaks: false })
const renderedNonBreaks = newmd.render(markdownFixtures.breaks) const renderedNonBreaks = newmd.render(markdownFixtures.breaks)
t.snapshot(renderedNonBreaks) expect(renderedNonBreaks).toMatchSnapshot()
}) })
test('Markdown.render() should renders abbrevations correctly', t => { test('Markdown.render() should renders abbrevations correctly', () => {
const rendered = md.render(markdownFixtures.abbrevations) const rendered = md.render(markdownFixtures.abbrevations)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })
test('Markdown.render() should renders sub correctly', t => { test('Markdown.render() should renders sub correctly', () => {
const rendered = md.render(markdownFixtures.subTexts) const rendered = md.render(markdownFixtures.subTexts)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })
test('Markdown.render() should renders sup correctly', t => { test('Markdown.render() should renders sup correctly', () => {
const rendered = md.render(markdownFixtures.supTexts) const rendered = md.render(markdownFixtures.supTexts)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })
test('Markdown.render() should renders definition lists correctly', t => { test('Markdown.render() should renders definition lists correctly', () => {
const rendered = md.render(markdownFixtures.deflists) const rendered = md.render(markdownFixtures.deflists)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })
test('Markdown.render() should render shortcuts correctly', t => { test('Markdown.render() should render shortcuts correctly', () => {
const rendered = md.render(markdownFixtures.shortcuts) const rendered = md.render(markdownFixtures.shortcuts)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })
test('Markdown.render() should render footnote correctly', t => { test('Markdown.render() should render footnote correctly', () => {
const rendered = md.render(markdownFixtures.footnote) const rendered = md.render(markdownFixtures.footnote)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })
test('Markdown.render() should renders [TOC] placholder correctly', t => { test('Markdown.render() should renders [TOC] placholder correctly', () => {
const rendered = md.render(markdownFixtures.tocPlaceholder) const rendered = md.render(markdownFixtures.tocPlaceholder)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })
test('Markdown.render() should render PlantUML MindMaps correctly', t => { test('Markdown.render() should render PlantUML MindMaps correctly', () => {
const rendered = md.render(markdownFixtures.plantUmlMindMap) const rendered = md.render(markdownFixtures.plantUmlMindMap)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })
test('Markdown.render() should render PlantUML Gantt correctly', t => { test('Markdown.render() should render PlantUML Gantt correctly', () => {
const rendered = md.render(markdownFixtures.plantUmlGantt) const rendered = md.render(markdownFixtures.plantUmlGantt)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })
test('Markdown.render() should render PlantUML WBS correctly', t => { test('Markdown.render() should render PlantUML WBS correctly', () => {
const rendered = md.render(markdownFixtures.plantUmlWbs) const rendered = md.render(markdownFixtures.plantUmlWbs)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })
test('Markdown.render() should render PlantUML Umls correctly', t => { test('Markdown.render() should render PlantUML Umls correctly', () => {
const rendered = md.render(markdownFixtures.plantUmlUml) const rendered = md.render(markdownFixtures.plantUmlUml)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })
test('Markdown.render() should render PlantUML Ditaa correctly', t => { test('Markdown.render() should render PlantUML Ditaa correctly', () => {
const rendered = md.render(markdownFixtures.plantUmlDitaa) const rendered = md.render(markdownFixtures.plantUmlDitaa)
t.snapshot(rendered) expect(rendered).toMatchSnapshot()
}) })

View File

@@ -1,19 +1,17 @@
/** /**
* @fileoverview Unit test for browser/lib/normalizeEditorFontFamily * @fileoverview Unit test for browser/lib/normalizeEditorFontFamily
*/ */
import test from 'ava'
import normalizeEditorFontFamily from '../../browser/lib/normalizeEditorFontFamily' import normalizeEditorFontFamily from '../../browser/lib/normalizeEditorFontFamily'
import consts from '../../browser/lib/consts' import consts from '../../browser/lib/consts'
const defaultEditorFontFamily = consts.DEFAULT_EDITOR_FONT_FAMILY const defaultEditorFontFamily = consts.DEFAULT_EDITOR_FONT_FAMILY
test('normalizeEditorFontFamily() should return default font family (string[])', t => { test('normalizeEditorFontFamily() should return default font family (string[])', () => {
t.is(normalizeEditorFontFamily(), defaultEditorFontFamily.join(', ')) expect(normalizeEditorFontFamily()).toBe(defaultEditorFontFamily.join(', '))
}) })
test('normalizeEditorFontFamily(["hoge", "huga"]) should return default font family connected with arg.', t => { test('normalizeEditorFontFamily(["hoge", "huga"]) should return default font family connected with arg.', () => {
const arg = 'font1, font2' const arg = 'font1, font2'
t.is( expect(normalizeEditorFontFamily(arg)).toBe(
normalizeEditorFontFamily(arg),
`${arg}, ${defaultEditorFontFamily.join(', ')}` `${arg}, ${defaultEditorFontFamily.join(', ')}`
) )
}) })

View File

@@ -1,9 +1,8 @@
const test = require('ava')
const path = require('path') const path = require('path')
const { parse } = require('browser/lib/RcParser') const { parse } = require('browser/lib/RcParser')
// Unit test // Unit test
test('RcParser should return a json object', t => { test('RcParser should return a json object', () => {
const validJson = { const validJson = {
editor: { keyMap: 'vim', switchPreview: 'BLUR', theme: 'monokai' }, editor: { keyMap: 'vim', switchPreview: 'BLUR', theme: 'monokai' },
hotkey: { toggleMain: 'Control + L' }, hotkey: { toggleMain: 'Control + L' },
@@ -51,20 +50,12 @@ test('RcParser should return a json object', t => {
validTestCases.forEach(validTestCase => { validTestCases.forEach(validTestCase => {
const [input, expected] = validTestCase const [input, expected] = validTestCase
t.is( expect(parse(filePath(input)).editor.keyMap).toBe(expected.editor.keyMap)
parse(filePath(input)).editor.keyMap,
expected.editor.keyMap,
`Test for getTodoStatus() input: ${input} expected: ${expected.keyMap}`
)
}) })
invalidTestCases.forEach(invalidTestCase => { invalidTestCases.forEach(invalidTestCase => {
const [input, expected] = invalidTestCase const [input, expected] = invalidTestCase
t.is( expect(parse(filePath(input)).editor).toBe(expected.editor)
parse(filePath(input)).editor,
expected.editor,
`Test for getTodoStatus() input: ${input} expected: ${expected.editor}`
)
}) })
}) })

View File

@@ -1,4 +1,3 @@
import test from 'ava'
import searchFromNotes from 'browser/lib/search' import searchFromNotes from 'browser/lib/search'
import { dummyNote } from '../fixtures/TestDummy' import { dummyNote } from '../fixtures/TestDummy'
import _ from 'lodash' import _ from 'lodash'
@@ -11,7 +10,7 @@ const pickContents = notes =>
let notes = [] let notes = []
let note1, note2, note3 let note1, note2, note3
test.before(t => { beforeAll(() => {
const data1 = { type: 'MARKDOWN_NOTE', content: 'content1', tags: ['tag1'] } const data1 = { type: 'MARKDOWN_NOTE', content: 'content1', tags: ['tag1'] }
const data2 = { const data2 = {
type: 'MARKDOWN_NOTE', type: 'MARKDOWN_NOTE',
@@ -27,7 +26,7 @@ test.before(t => {
notes = [note1, note2, note3] notes = [note1, note2, note3]
}) })
test('it can find notes by tags and words', t => { test('it can find notes by tags and words', () => {
// [input, expected content (Array)] // [input, expected content (Array)]
const testWithTags = [ const testWithTags = [
['#tag1', [note1.content, note2.content, note3.content]], ['#tag1', [note1.content, note2.content, note3.content]],
@@ -49,6 +48,8 @@ test('it can find notes by tags and words', t => {
testCases.forEach(testCase => { testCases.forEach(testCase => {
const [input, expectedContents] = testCase const [input, expectedContents] = testCase
const results = searchFromNotes(notes, input) const results = searchFromNotes(notes, input)
t.true(_.isEqual(pickContents(results).sort(), expectedContents.sort())) expect(
_.isEqual(pickContents(results).sort(), expectedContents.sort())
).toBe(true)
}) })
}) })

View File

@@ -1,58 +1,57 @@
import test from 'ava'
import slugify from 'browser/lib/slugify' import slugify from 'browser/lib/slugify'
test('alphabet and digit', t => { test('alphabet and digit', () => {
const upperAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' const upperAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
const lowerAlphabet = 'abcdefghijklmnopqrstuvwxyz' const lowerAlphabet = 'abcdefghijklmnopqrstuvwxyz'
const digit = '0123456789' const digit = '0123456789'
const testCase = upperAlphabet + lowerAlphabet + digit const testCase = upperAlphabet + lowerAlphabet + digit
const decodeSlug = decodeURI(slugify(testCase)) const decodeSlug = decodeURI(slugify(testCase))
t.true(decodeSlug === testCase) expect(decodeSlug === testCase).toBe(true)
}) })
test('should delete unavailable symbols', t => { test('should delete unavailable symbols', () => {
const availableSymbols = '_-' const availableSymbols = '_-'
const testCase = availableSymbols + "][!'#$%&()*+,./:;<=>?@\\^{|}~`" const testCase = availableSymbols + "][!'#$%&()*+,./:;<=>?@\\^{|}~`"
const decodeSlug = decodeURI(slugify(testCase)) const decodeSlug = decodeURI(slugify(testCase))
t.true(decodeSlug === availableSymbols) expect(decodeSlug === availableSymbols).toBe(true)
}) })
test('should convert from white spaces between words to hyphens', t => { test('should convert from white spaces between words to hyphens', () => {
const testCase = 'This is one' const testCase = 'This is one'
const expectedString = 'This-is-one' const expectedString = 'This-is-one'
const decodeSlug = decodeURI(slugify(testCase)) const decodeSlug = decodeURI(slugify(testCase))
t.true(decodeSlug === expectedString) expect(decodeSlug === expectedString).toBe(true)
}) })
test('should remove leading white spaces', t => { test('should remove leading white spaces', () => {
const testCase = ' This is one' const testCase = ' This is one'
const expectedString = 'This-is-one' const expectedString = 'This-is-one'
const decodeSlug = decodeURI(slugify(testCase)) const decodeSlug = decodeURI(slugify(testCase))
t.true(decodeSlug === expectedString) expect(decodeSlug === expectedString).toBe(true)
}) })
test('should remove trailing white spaces', t => { test('should remove trailing white spaces', () => {
const testCase = 'This is one ' const testCase = 'This is one '
const expectedString = 'This-is-one' const expectedString = 'This-is-one'
const decodeSlug = decodeURI(slugify(testCase)) const decodeSlug = decodeURI(slugify(testCase))
t.true(decodeSlug === expectedString) expect(decodeSlug === expectedString).toBe(true)
}) })
test('2-byte charactor support', t => { test('2-byte charactor support', () => {
const testCase = '菠萝芒果テストÀžƁƵ' const testCase = '菠萝芒果テストÀžƁƵ'
const decodeSlug = decodeURI(slugify(testCase)) const decodeSlug = decodeURI(slugify(testCase))
t.true(decodeSlug === testCase) expect(decodeSlug === testCase).toBe(true)
}) })
test('emoji', t => { test('emoji', () => {
const testCase = '🌸' const testCase = '🌸'
const decodeSlug = decodeURI(slugify(testCase)) const decodeSlug = decodeURI(slugify(testCase))
t.true(decodeSlug === testCase) expect(decodeSlug === testCase).toBe(true)
}) })

View File

@@ -0,0 +1,103 @@
/**
* @fileoverview Unit test for browser/main/lib/ThemeManager.js
*/
const { chooseTheme, applyTheme } = require('browser/main/lib/ThemeManager')
jest.mock('../../browser/main/lib/ConfigManager', () => {
return {
set: () => {}
}
})
const originalDate = Date
let context = {}
beforeAll(() => {
const constantDate = new Date('2017-11-27T14:33:42')
global.Date = class extends Date {
constructor() {
super()
return constantDate
}
}
})
beforeEach(() => {
context = {
ui: {
theme: 'white',
scheduledTheme: 'dark',
enableScheduleTheme: true,
defaultTheme: 'monokai'
}
}
})
afterAll(() => {
global.Date = originalDate
})
test("enableScheduleTheme is false, theme shouldn't change", () => {
context.ui.enableScheduleTheme = false
const beforeTheme = context.ui.theme
chooseTheme(context)
const afterTheme = context.ui.theme
expect(afterTheme).toBe(beforeTheme)
})
// NOT IN SCHEDULE
test("scheduleEnd is bigger than scheduleStart and not in schedule, theme shouldn't change", () => {
const beforeTheme = context.ui.defaultTheme
context.ui.scheduleStart = 720 // 12:00
context.ui.scheduleEnd = 870 // 14:30
chooseTheme(context)
const afterTheme = context.ui.theme
expect(afterTheme).toBe(beforeTheme)
})
test("scheduleStart is bigger than scheduleEnd and not in schedule, theme shouldn't change", () => {
const beforeTheme = context.ui.defaultTheme
context.ui.scheduleStart = 960 // 16:00
context.ui.scheduleEnd = 600 // 10:00
chooseTheme(context)
const afterTheme = context.ui.theme
expect(afterTheme).toBe(beforeTheme)
})
// IN SCHEDULE
test('scheduleEnd is bigger than scheduleStart and in schedule, theme should change', () => {
const beforeTheme = context.ui.scheduledTheme
context.ui.scheduleStart = 720 // 12:00
context.ui.scheduleEnd = 900 // 15:00
chooseTheme(context)
const afterTheme = context.ui.theme
expect(afterTheme).toBe(beforeTheme)
})
test('scheduleStart is bigger than scheduleEnd and in schedule, theme should change', () => {
const beforeTheme = context.ui.scheduledTheme
context.ui.scheduleStart = 1200 // 20:00
context.ui.scheduleEnd = 900 // 15:00
chooseTheme(context)
const afterTheme = context.ui.theme
expect(afterTheme).toBe(beforeTheme)
})
test("theme to apply is not a supported theme, theme shouldn't change", () => {
applyTheme('notATheme')
const afterTheme = document.body.dataset.theme
expect(afterTheme).toBe('default')
})
test('theme to apply is a supported theme, theme should change', () => {
applyTheme(context.ui.defaultTheme)
const afterTheme = document.body.dataset.theme
expect(afterTheme).toBe(context.ui.defaultTheme)
})

159
yarn.lock
View File

@@ -1966,6 +1966,11 @@ combined-stream@1.0.6, combined-stream@~1.0.5:
dependencies: dependencies:
delayed-stream "~1.0.0" delayed-stream "~1.0.0"
command-exists@^1.2.9:
version "1.2.9"
resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==
commander@2: commander@2:
version "2.16.0" version "2.16.0"
resolved "http://registry.npm.taobao.org/commander/download/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50" resolved "http://registry.npm.taobao.org/commander/download/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50"
@@ -2583,7 +2588,44 @@ d3-zoom@1:
d3-selection "1" d3-selection "1"
d3-transition "1" d3-transition "1"
d3@^5.12, d3@^5.7.0: d3@^5.14:
version "5.16.0"
resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877"
integrity sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==
dependencies:
d3-array "1"
d3-axis "1"
d3-brush "1"
d3-chord "1"
d3-collection "1"
d3-color "1"
d3-contour "1"
d3-dispatch "1"
d3-drag "1"
d3-dsv "1"
d3-ease "1"
d3-fetch "1"
d3-force "1"
d3-format "1"
d3-geo "1"
d3-hierarchy "1"
d3-interpolate "1"
d3-path "1"
d3-polygon "1"
d3-quadtree "1"
d3-random "1"
d3-scale "2"
d3-scale-chromatic "1"
d3-selection "1"
d3-shape "1"
d3-time "1"
d3-time-format "2"
d3-timer "1"
d3-transition "1"
d3-voronoi "1"
d3-zoom "1"
d3@^5.7.0:
version "5.12.0" version "5.12.0"
resolved "https://registry.yarnpkg.com/d3/-/d3-5.12.0.tgz#0ddeac879c28c882317cd439b495290acd59ab61" resolved "https://registry.yarnpkg.com/d3/-/d3-5.12.0.tgz#0ddeac879c28c882317cd439b495290acd59ab61"
integrity sha512-flYVMoVuhPFHd9zVCe2BxIszUWqBcd5fvQGMNRmSiBrgdnh6Vlruh60RJQTouAK9xPbOB0plxMvBm4MoyODXNg== integrity sha512-flYVMoVuhPFHd9zVCe2BxIszUWqBcd5fvQGMNRmSiBrgdnh6Vlruh60RJQTouAK9xPbOB0plxMvBm4MoyODXNg==
@@ -2626,13 +2668,14 @@ d@1:
dependencies: dependencies:
es5-ext "^0.10.9" es5-ext "^0.10.9"
dagre-d3@dagrejs/dagre-d3: dagre-d3@^0.6.4:
version "0.6.4-pre" version "0.6.4"
resolved "https://codeload.github.com/dagrejs/dagre-d3/tar.gz/e1a00e5cb518f5d2304a35647e024f31d178e55b" resolved "https://registry.yarnpkg.com/dagre-d3/-/dagre-d3-0.6.4.tgz#0728d5ce7f177ca2337df141ceb60fbe6eeb7b29"
integrity sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==
dependencies: dependencies:
d3 "^5.12" d3 "^5.14"
dagre "^0.8.4" dagre "^0.8.5"
graphlib "^2.1.7" graphlib "^2.1.8"
lodash "^4.17.15" lodash "^4.17.15"
dagre@^0.8.4: dagre@^0.8.4:
@@ -2643,6 +2686,14 @@ dagre@^0.8.4:
graphlib "^2.1.7" graphlib "^2.1.7"
lodash "^4.17.4" lodash "^4.17.4"
dagre@^0.8.5:
version "0.8.5"
resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==
dependencies:
graphlib "^2.1.8"
lodash "^4.17.15"
dashdash@^1.12.0: dashdash@^1.12.0:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -3130,6 +3181,13 @@ entities@^1.1.1, entities@~1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
entity-decode@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/entity-decode/-/entity-decode-2.0.2.tgz#e4f807e52c3294246e9347d1f2b02b07fd5f92e7"
integrity sha512-5CCY/3ci4MC1m2jlumNjWd7VBFt4VfFnmSqSNmVcXq4gxM3Vmarxtt+SvmBnzwLS669MWdVuXboNVj1qN2esVg==
dependencies:
he "^1.1.1"
env-paths@^1.0.0: env-paths@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0"
@@ -4302,6 +4360,13 @@ graphlib@^2.1.7:
dependencies: dependencies:
lodash "^4.17.5" lodash "^4.17.5"
graphlib@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da"
integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==
dependencies:
lodash "^4.17.15"
gray-matter@^2.1.0: gray-matter@^2.1.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-2.1.1.tgz#3042d9adec2a1ded6a7707a9ed2380f8a17a430e" resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-2.1.1.tgz#3042d9adec2a1ded6a7707a9ed2380f8a17a430e"
@@ -4490,7 +4555,7 @@ has@^1.0.1:
dependencies: dependencies:
function-bind "^1.0.2" function-bind "^1.0.2"
he@^1.2.0: he@^1.1.1, he@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
@@ -5987,8 +6052,9 @@ locate-path@^3.0.0:
path-exists "^3.0.0" path-exists "^3.0.0"
lodash-es@^4.2.1: lodash-es@^4.2.1:
version "4.17.10" version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
lodash-move@^1.1.1: lodash-move@^1.1.1:
version "1.1.1" version "1.1.1"
@@ -6001,9 +6067,10 @@ lodash._getnative@^3.0.0:
resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
lodash._reinterpolate@~3.0.0: lodash._reinterpolate@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
lodash.clonedeep@^4.5.0: lodash.clonedeep@^4.5.0:
version "4.5.0" version "4.5.0"
@@ -6075,8 +6142,9 @@ lodash.merge@^4.6.0:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
lodash.mergewith@^4.6.0: lodash.mergewith@^4.6.0:
version "4.6.1" version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
lodash.some@^4.5.1: lodash.some@^4.5.1:
version "4.6.0" version "4.6.0"
@@ -6087,31 +6155,28 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
lodash.template@^4.2.2, lodash.template@^4.3.0: lodash.template@^4.2.2, lodash.template@^4.3.0:
version "4.4.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==
dependencies: dependencies:
lodash._reinterpolate "~3.0.0" lodash._reinterpolate "^3.0.0"
lodash.templatesettings "^4.0.0" lodash.templatesettings "^4.0.0"
lodash.templatesettings@^4.0.0: lodash.templatesettings@^4.0.0:
version "4.1.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316" resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33"
integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==
dependencies: dependencies:
lodash._reinterpolate "~3.0.0" lodash._reinterpolate "^3.0.0"
lodash.uniq@^4.5.0: lodash.uniq@^4.5.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
lodash@^4.0.0, lodash@^4.0.1, lodash@^4.12.0, lodash@^4.13.1, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1: lodash@^4.0.0, lodash@^4.0.1, lodash@^4.12.0, lodash@^4.13.0, lodash@^4.13.1, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1:
version "4.17.13" version "4.17.19"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
integrity sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA== integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
lodash@^4.13.0, lodash@^4.17.11, lodash@^4.17.15:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
lodash@~0.9.2: lodash@~0.9.2:
version "0.9.2" version "0.9.2"
@@ -6395,22 +6460,21 @@ merge@^1.1.3:
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
mermaid@^8.4.2: mermaid@^8.5.2:
version "8.4.2" version "8.5.2"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.4.2.tgz#91d3d8e9541e72eed7a78d0e882db11564fab3bb" resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.5.2.tgz#0f1914cda53d4ea5377380e5ce07a38bef2ea7e8"
integrity sha512-vYSCP2u4XkOnjliWz/QIYwvzF/znQAq22vWJJ3YV40SnwV2JQyHblnwwNYXCprkXw7XfwBKDpSNaJ3HP4WfnZw== integrity sha512-I+s+8/RzlazF3dGOhDUfU/ERkUV4zfIlTWb3703jNx+2lfACs+4AdY9ULQaw6BPWzW3gB+XlXFOOX/m/vqujIA==
dependencies: dependencies:
"@braintree/sanitize-url" "^3.1.0" "@braintree/sanitize-url" "^3.1.0"
crypto-random-string "^3.0.1" crypto-random-string "^3.0.1"
d3 "^5.7.0" d3 "^5.7.0"
dagre "^0.8.4" dagre "^0.8.4"
dagre-d3 dagrejs/dagre-d3 dagre-d3 "^0.6.4"
entity-decode "^2.0.2"
graphlib "^2.1.7" graphlib "^2.1.7"
he "^1.2.0" he "^1.2.0"
lodash "^4.17.11"
minify "^4.1.1" minify "^4.1.1"
moment-mini "^2.22.1" moment-mini "^2.22.1"
prettier "^1.18.2"
scope-css "^1.2.1" scope-css "^1.2.1"
methods@~1.1.2: methods@~1.1.2:
@@ -7205,8 +7269,9 @@ path-key@^2.0.0, path-key@^2.0.1:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
path-parse@^1.0.5: path-parse@^1.0.5:
version "1.0.5" version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-to-regexp@0.1.7: path-to-regexp@0.1.7:
version "0.1.7" version "0.1.7"
@@ -7777,9 +7842,10 @@ querystring@0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
querystringify@^2.0.0: querystringify@^2.1.1:
version "2.0.0" version "2.1.1"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.0.0.tgz#fa3ed6e68eb15159457c89b37bc6472833195755" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e"
integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==
randomatic@^3.0.0: randomatic@^3.0.0:
version "3.0.0" version "3.0.0"
@@ -8421,6 +8487,7 @@ require-uncached@^1.0.2, require-uncached@^1.0.3:
requires-port@^1.0.0: requires-port@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
resolve-cwd@^2.0.0: resolve-cwd@^2.0.0:
version "2.0.0" version "2.0.0"
@@ -9815,10 +9882,11 @@ url-parse-lax@^1.0.0:
prepend-http "^1.0.1" prepend-http "^1.0.1"
url-parse@^1.1.8, url-parse@~1.4.0: url-parse@^1.1.8, url-parse@~1.4.0:
version "1.4.0" version "1.4.7"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.0.tgz#6bfdaad60098c7fe06f623e42b22de62de0d3d75" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278"
integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==
dependencies: dependencies:
querystringify "^2.0.0" querystringify "^2.1.1"
requires-port "^1.0.0" requires-port "^1.0.0"
url@0.10.3: url@0.10.3:
@@ -10019,8 +10087,9 @@ websocket-driver@>=0.5.1:
websocket-extensions ">=0.1.1" websocket-extensions ">=0.1.1"
websocket-extensions@>=0.1.1: websocket-extensions@>=0.1.1:
version "0.1.3" version "0.1.4"
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
well-known-symbols@^1.0.0: well-known-symbols@^1.0.0:
version "1.0.0" version "1.0.0"