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

Compare commits

...

161 Commits

Author SHA1 Message Date
Junyoung Choi
6a2242725d v0.11.9 2018-09-04 16:11:27 +09:00
Junyoung Choi (Sai)
1117e1b724 Merge pull request #2357 from BoostIO/revert-flickering-fix
Revert flickering fix
2018-09-01 16:56:11 +09:00
Junyoung Choi
d015b18c66 Revert flickering fix 2018-09-01 16:50:52 +09:00
Junyoung Choi
039f73711a Update yarn.lock 2018-08-20 15:22:28 +09:00
Junyoung Choi (Sai)
6e885acf8c Merge pull request #2311 from BoostIO/update-build-document
Update build documents
2018-08-20 15:20:31 +09:00
Junyoung Choi
9ef07cea7a Update build documents 2018-08-20 15:19:40 +09:00
Junyoung Choi (Sai)
9e3b321aaf Merge pull request #2216 from ZeroX-DG/improve-dev-script
Improved dev script
2018-08-20 15:05:21 +09:00
Junyoung Choi (Sai)
21560701ea Merge pull request #2303 from daiyam/update-electron
update electron version
2018-08-20 14:58:49 +09:00
Junyoung Choi (Sai)
4556375174 Merge pull request #2284 from mikaoelitiana/feat-933
Add per-folder sort
2018-08-20 13:46:03 +09:00
Junyoung Choi (Sai)
91b5398b5a Merge pull request #2307 from modmod24/fix-todo-percentage
update todo percentage correctly #2267
2018-08-20 12:00:42 +09:00
Junyoung Choi (Sai)
eeb8016992 Merge pull request #2302 from daiyam/window-position
restore window position
2018-08-20 11:57:47 +09:00
Junyoung Choi (Sai)
736106be3a Merge pull request #2283 from ZeroX-DG/fix-snippet-list-bug
Fixed snippet list bug
2018-08-20 10:35:06 +09:00
Junyoung Choi (Sai)
f400568dc0 Merge pull request #2272 from ZeroX-DG/improve-theme
Improve sideNav scroll bar color
2018-08-20 10:24:48 +09:00
Junyoung Choi (Sai)
0ca96cba6e Merge pull request #2074 from max-buranbaev/blinking-markdown-crunch-fix
Blinking markdown crunch fix
2018-08-20 10:05:06 +09:00
Unknown
df4d837026 add todo status test 2018-08-18 14:38:07 +01:00
Unknown
760f84d7fa fix for 2267
todo percentage not updated correctly
2018-08-18 14:30:23 +01:00
Baptiste Augrain
174a315e3f update electron version 2018-08-16 01:23:35 +02:00
Baptiste Augrain
0834313456 restore window position 2018-08-16 01:06:13 +02:00
Unknown
df931e10c0 Merge remote-tracking branch 'BoostIO/master' 2018-08-12 17:53:03 +01:00
Mika Andrianarijaona
9572cb2d33 Fix default value of config.sortBy 2018-08-12 09:21:46 +02:00
Max Buranbaev
51e836f32a code style 2018-08-11 16:17:56 +05:00
Max Buranbaev
7fefbd88d0 Resolving conflict 2018-08-11 11:13:04 +00:00
Mika Andrianarijaona
cb956c5508 Use default value config.sortBy
- for new folders
- for folders with no config set
2018-08-11 11:44:22 +02:00
Mika Andrianarijaona
47b0086bf8 Add per-folder sort
- save sort configuration in `config.[folderKey].sortBy`
- use lodash ` _.get(config, [folderKey, 'sortBy'])` to avoid error
2018-08-11 11:36:36 +02:00
Nguyễn Việt Hưng
b8d66e4a95 fixed snippet list bug 2018-08-11 09:20:04 +07:00
Max Buranbaev
bfc1c93153 Merge branch 'master' into blinking-markdown-crunch-fix 2018-08-10 19:02:43 +05:00
Nguyễn Việt Hưng
404faf8a0b limited style for just side nav 2018-08-09 22:59:40 +07:00
Nguyễn Việt Hưng
4a7b0f4711 improved scroll bar color for default theme 2018-08-09 22:57:16 +07:00
Nguyen Viet Hung
dd62fca45d change dev-start to dev 2018-08-09 16:21:27 +07:00
Junyoung Choi (Sai)
79fb04126c Merge pull request #2214 from cdayjr/master
Fix for BoostIO/Boostnote#2204
2018-08-09 18:15:20 +09:00
Junyoung Choi (Sai)
39c9574ae3 Merge pull request #2257 from ehhc/Attachments_in_markdown_export_#1786
might fixes #1786 --> export attachments for markdown notes
2018-08-09 18:10:48 +09:00
Junyoung Choi (Sai)
38af257adf Merge pull request #2253 from mbarczak/master
Fix for issue #2088 :  snippet notes are not displaying in markdown
2018-08-09 18:07:29 +09:00
Junyoung Choi (Sai)
5aae9a4722 Merge pull request #2231 from yougotwill/mermaid_dark_theme_fix
Mermaid dark theme rendering fix
2018-08-09 17:54:54 +09:00
Junyoung Choi (Sai)
cfe3cae88d Merge pull request #2226 from sklein12/addSnippetToSearchScope
Add code snippets to search scope
2018-08-09 17:54:28 +09:00
Junyoung Choi (Sai)
612de84ac6 Merge pull request #2208 from enyaxu/feature/2132
New Feature: Shortcuts for focusing tag editor(CmdOrControl+Shift+T)(#2132)
2018-08-09 17:53:54 +09:00
Nguyen Viet Hung
33be597ef0 Delete package-lock.json 2018-08-09 15:50:07 +07:00
Junyoung Choi (Sai)
cc26fd80d7 Merge pull request #2235 from amedora/feature/1454-ditaa
add Ditaa support
2018-08-09 17:47:37 +09:00
Junyoung Choi (Sai)
c227a1ffec Merge pull request #2171 from yamash723/fixbug-html-export
Fix search value for html export path
2018-08-09 17:43:56 +09:00
Nguyen Viet Hung
f0df787bbe Fix escape codeblock (#2230)
* updated package-lock

* added fix and test for escape html in code block

* fixed markdown preview render bug

* updated comment in escape function

* improved escape function

* Delete package-lock.json
2018-08-09 17:08:52 +09:00
ehhc
09188bed48 might fixes #1786 --> export attachments for markdown notes 2018-07-30 18:09:02 +02:00
Maciek
4a3bcaba06 BUG FIX: Change the way of checking for empty array
The original condition : attachments !== [] always returns true,
for empty array, as well as for array with elements.
2018-07-28 22:25:10 +02:00
Maciek
1d1ab65edd BUG FIX: snippet notes are not displaying in markdown #2088
Fix for issue https://github.com/BoostIo/Boostnote/issues/2088.
In specific situation, when all below conditions are met :
- one of the snippets notes tabs is a Markdown tab,
- the migrateAttachments code is called on a snippet note,
- historical attachments location '/images' exists in snippet storage
  folder

Following exception is being thrown :
path.js:28 Uncaught TypeError: Path must be a string. Received undefined
The exception is a result of an undefined noteKey variable
(which is defined only for Markdown notes).
The solution is to skip migration of attachments for notes without
noteKey (which wouldn't be possible anyway, since noteKey is a
necessary for creating folder for attachments).
2018-07-28 22:23:14 +02:00
Junyoung Choi (Sai)
7330cdaf1c Merge pull request #2209 from yougotwill/info_box_fix
Trash infoPanel Fix
2018-07-28 23:34:46 +09:00
Junyoung Choi (Sai)
050a1fb6cf Merge pull request #2239 from narukami894/improve_jp_build_md
add and improve translation, fix typo
2018-07-28 23:33:56 +09:00
narukami894
1e8397cf17 add and improve translation, fix typo 2018-07-24 16:01:41 +09:00
amedora
59b53ece2b add Ditaa support (resolve #1454) 2018-07-23 11:16:20 +09:00
William Grant
16c62cd46f changed double quotes to single quotes 2018-07-21 19:31:40 +10:00
William Grant
eff56c2514 mermaid diagram rendering for dark themes is now fixed 2018-07-21 19:20:20 +10:00
yamash723
ee6b9a223f Change output path format of html file by platform 2018-07-21 15:25:09 +09:00
Chad Wade Day, Jr
acc6ea434a Merge remote-tracking branch 'upstream/master' 2018-07-20 11:45:36 -07:00
Steve Klein
1e5a7356f4 Add code snippets to search scope 2018-07-20 02:00:11 -07:00
Nguyễn Việt Hưng
4c8342c19d updated dev script 2018-07-19 17:04:55 +07:00
Nguyễn Việt Hưng
dad5232ecb updated package-lock 2018-07-19 16:55:19 +07:00
Junyoung Choi
6cad2ab4df v0.11.8 2018-07-19 15:21:35 +09:00
Chad Wade Day, Jr
be972781ee Fix for BoostIO/Boostnote#2204 2018-07-18 11:58:54 -07:00
William Grant
58fbc298b1 Merge branch 'master' into info_box_fix 2018-07-17 14:41:45 +02:00
William Grant
7de7772339 Fixed infoButton panel in trash positioning 2018-07-17 14:41:22 +02:00
JianXu
ad847a2f5d New Feature: Shortcuts for focusing tag editor(CmdOrControl+T) 2018-07-17 17:19:03 +08:00
Junyoung Choi (Sai)
856d52891c Merge pull request #2205 from saaguero/fix-attachment
Fix attachment interoperability between win and *nix
2018-07-17 16:23:16 +09:00
Junyoung Choi
8de3b3bd8d Update yarn.lock 2018-07-17 15:55:03 +09:00
Junyoung Choi (Sai)
0414483be2 Merge pull request #2178 from enyaxu/feature/2165
Hotkey for toggle editor fullscreen
2018-07-17 14:49:03 +09:00
Junyoung Choi (Sai)
22939aa472 Merge pull request #2145 from amedora/table-formatter
Markdown Table Formatter
2018-07-17 13:50:00 +09:00
Junyoung Choi (Sai)
0cb7c44985 Merge pull request #2168 from enyaxu/bug/2018
Fixed 'Focus Search' shortcut
2018-07-17 13:35:44 +09:00
Junyoung Choi (Sai)
b18a09e5eb Merge pull request #1935 from ZeroX-DG/allow-no-html-escape
Allow customizing html escape when export note
2018-07-17 12:38:14 +09:00
Junyoung Choi (Sai)
ef3649b1d6 Merge pull request #2172 from enyaxu/feature/862
Add chartjs support
2018-07-17 12:21:31 +09:00
Junyoung Choi (Sai)
ac70a0d94d Merge branch 'master' into feature/862 2018-07-17 12:21:17 +09:00
Junyoung Choi (Sai)
3b91f9b88b Merge pull request #2173 from enyaxu/feature/389
Add mermaid support
2018-07-17 12:15:36 +09:00
Santiago Agüero
c37b780ca4 Use markdown content for migrateAttachments 2018-07-17 00:13:13 -03:00
Junyoung Choi (Sai)
20061d2c65 Merge pull request #2183 from max-buranbaev/flickering-on-create-note
Flickering on create note
2018-07-17 11:54:33 +09:00
Junyoung Choi (Sai)
f18fa77c1c Merge pull request #2187 from ivanovserge/issue-2156
Issue 2156
2018-07-17 11:49:39 +09:00
Junyoung Choi (Sai)
a4c6869d4d Merge pull request #2192 from enyaxu/improvement/2133
Replace shortcut for 'Next Note' and 'Previous Note'
2018-07-17 11:46:08 +09:00
Junyoung Choi (Sai)
f9a0070c82 Merge pull request #2197 from chang/improve-bracket-completion
Improve bracket autoclosing
2018-07-17 11:20:27 +09:00
Santiago Agüero
5cc52f91cb Fix lint errors 2018-07-15 12:07:27 -03:00
Santiago Agüero
a46b9fb2be Fix attachment interop between win and nix 2018-07-15 01:37:47 -03:00
Eric
933e38eca9 improve bracket autoclosing 2018-07-09 18:37:54 -05:00
JianXu
e182390480 Replace shortcut for 'Next Note' and 'Previous Note' 2018-07-07 23:02:17 +08:00
Nguyễn Việt Hưng
563fdcba94 added tests escape html function 2018-07-07 00:07:17 +07:00
Nguyễn Việt Hưng
bc640834cd allow detect code block or not in escapeHtml function 2018-07-06 23:45:18 +07:00
Junyoung Choi (Sai)
0e9e7d644a Merge pull request #2191 from BoostIO/fix-codefence-xss
Add sanitization for code fence
2018-07-07 01:23:23 +09:00
Junyoung Choi
1d9b3ac2b5 Add sanitization for code fence 2018-07-07 01:22:11 +09:00
Junyoung Choi (Sai)
aebed4a644 Merge pull request #2033 from ZeroX-DG/fix-scrollbar-disappear
fixed disappearing scroll bar
2018-07-07 00:47:14 +09:00
Junyoung Choi
7bfb094a40 use lighter color for scroll bar 2018-07-06 22:07:06 +09:00
Kazz Yokomizo
f90a44c1d0 Merge pull request #2189 from BoostIO/update-readme
Update readme
2018-07-06 15:55:25 +09:00
Kazz Yokomizo
dfcf6d2729 Update readme 2018-07-06 15:37:10 +09:00
Сергей Иванов
806a5daa86 Processing all location's pathnames 2018-07-05 11:23:57 +03:00
Сергей Иванов
4a3602099a Difference home and searched notes from trashed units 2018-07-04 16:09:49 +03:00
Сергей Иванов
c69be54655 Fixing empty string searching 2018-07-04 14:02:26 +03:00
Сергей Иванов
680eaa1d4a Filtering displayed notes in Detail component 2018-07-04 13:33:47 +03:00
Nguyễn Việt Hưng
9cc7b8bcc6 fixed redundant code 2018-07-04 15:35:46 +07:00
Nguyễn Việt Hưng
55d86d853a improved escape function 2018-07-04 15:27:30 +07:00
Nguyễn Việt Hưng
7d9f309e04 Merge remote-tracking branch 'upstream/master' into allow-no-html-escape 2018-07-04 13:50:10 +07:00
Nguyễn Việt Hưng
c2f0147cff updated new escape html function 2018-07-04 13:50:05 +07:00
JianXu
05488e66ae Add tooltip(CommandOrCtrl+B) for fullscreen 2018-07-04 11:38:43 +08:00
JianXu
09eac89086 Hotkey for toggle editor fullscreen 2018-07-04 09:59:06 +08:00
JianXu
866a0e7534 Add mermaid support 2018-07-03 15:10:08 +08:00
Max Buranbaev
3c8337cf54 adding timeout one creating a snippet 2018-07-03 10:29:22 +05:00
Max Buranbaev
883b4c4c26 adding timeout on creating a note 2018-07-03 10:27:13 +05:00
JianXu
6bc42c564d Add chartjs 2018-07-03 12:58:45 +08:00
yamash723
4f79f52524 Fix search value for html export path 2018-07-03 10:03:31 +09:00
JianXu
d2b2e76a6a Fixed 'Focus Search' shortcut 2018-07-02 21:43:46 +08:00
Sosuke Suzuki
9d9109e9e5 Merge pull request #2158 from BoostIO/fix-contextmenu-bug
Fix contextmenu bug
2018-07-02 11:27:28 +09:00
Kazz Yokomizo
18efb89b9a Merge pull request #2163 from BoostIO/update-slack
Update slack invitation url
2018-07-01 22:02:52 +09:00
Kazz Yokomizo
cefe883025 Update slack invitation url 2018-07-01 21:59:08 +09:00
Sosuke Suzuki
0ffa0b96d3 Merge pull request #2135 from BoostIO/improve-snippets-ui
Improve snippets ui
2018-06-30 20:11:33 +09:00
Sosuke Suzuki
0429acfa1b Merge pull request #2138 from BoostIO/improve-uitab-editor
Improve uitab editor
2018-06-30 20:11:20 +09:00
Sosuke Suzuki
827e3c1829 Merge pull request #2144 from kelvin-wong/retain-collapsed-storage-state
Add collapsed state for storage
2018-06-30 20:11:01 +09:00
Sosuke Suzuki
aa756ef194 Merge pull request #2146 from BoostIO/update-codemirror
update codemirror
2018-06-30 20:10:13 +09:00
Sosuke Suzuki
d8aad65b24 fix from eslint 2018-06-30 16:28:17 +09:00
Sosuke Suzuki
1038e86196 use context.popup on StatusBar 2018-06-30 16:25:44 +09:00
Sosuke Suzuki
47845fd4e3 use context.popup on SideNav 2018-06-30 16:23:51 +09:00
Sosuke Suzuki
294c3f10ab use context.popup on StorageItem 2018-06-30 16:21:35 +09:00
Sosuke Suzuki
f6afc756dc use context.popup on SnippetList 2018-06-30 16:16:18 +09:00
Sosuke Suzuki
64407e5ca6 use context.popup on SnippetNoteDetai; 2018-06-30 16:12:23 +09:00
Sosuke Suzuki
0a42b0f61f use context.popup on NoteList 2018-06-30 16:08:13 +09:00
Junyoung Choi
ae493cbd0e v0.11.7 2018-06-30 00:51:15 +09:00
Kelvin Wong
ddd339851b Fix code style 2018-06-29 10:58:11 +08:00
amedora
82db986bd7 add 'Format Table' in the File menu 2018-06-28 18:16:38 +09:00
amedora
7bacd6f8f0 CodeEditor can handle 'code:format-table' event 2018-06-28 18:16:38 +09:00
amedora
f0941f47dd create TableEditor when CodeEditor mounted 2018-06-28 18:09:06 +09:00
amedora
c9d05b1117 CodeEditor use TableEditor 2018-06-28 18:09:06 +09:00
amedora
7ee12752ec CodeEditor use TextEditorInterface 2018-06-28 18:09:06 +09:00
amedora
b44772441d implement TextEditorInterface 2018-06-28 18:09:06 +09:00
Sosuke Suzuki
72e3784fa5 update codemirror 2018-06-28 17:37:36 +09:00
Max Buranbaev
2c7f24cb8c Debouncing of rendering due to flickering fix 2018-06-28 13:03:12 +05:00
Kelvin Wong
8a6c86bf65 Add collapsed state for storage
The root cause of this issue is that when the folder is clicked,
the router pushed the path and the StorageItem component has been
refreshed and isOpen has been reset

- Add storing collapse state for storage
- Add tests
- Default as collapsed for fallback

fix BoostIo/Boostnote#1979 BoostIo/Boostnote#1911
2018-06-28 13:08:39 +08:00
amedora
cc52cf60dc add reference to @susisu/mte-kernel 2018-06-28 13:36:04 +09:00
Max Buranbaev
398ebae2ba Merge branch 'master' into blinking-markdown-crunch-fix 2018-06-27 14:21:34 +00:00
Sosuke Suzuki
0095735841 fix from eslint 2018-06-27 20:42:19 +09:00
Sosuke Suzuki
95c10a1de7 add test 2018-06-27 20:42:01 +09:00
Sosuke Suzuki
8f4c92e251 extract normalizeEditorFonrFamily 2018-06-27 19:48:09 +09:00
Sosuke Suzuki
58354061d8 set font-family to editor in Preference > Interface 2018-06-27 19:34:58 +09:00
Max Buranbaev
5de176757d Fixing flickering in both cases 2018-06-27 13:07:38 +05:00
Sosuke Suzuki
7414d52dc2 selected snippet item background-color is darken 2018-06-27 16:27:55 +09:00
Sosuke Suzuki
c42b5c8806 First snippet is selected when open Snippets tab 2018-06-27 16:13:42 +09:00
Sosuke Suzuki
5c60da0f8f apply style to each themes button 2018-06-27 15:59:14 +09:00
Junyoung Choi (Sai)
70d02e9a6d Merge pull request #2125 from johannbre/admonition_bug_fix
Fix rendering of admonitions in Windows
2018-06-27 14:50:54 +09:00
Junyoung Choi (Sai)
6401016424 Merge pull request #2122 from yosmoc/plant_uml_improvement
plantuml non-english character support and timing diagram support
2018-06-27 04:54:21 +09:00
Junyoung Choi (Sai)
cd031a89fb Merge pull request #2105 from romainwn/docs/add-missing-translations-fr
update fr translation
2018-06-27 04:52:52 +09:00
Junyoung Choi (Sai)
aadba79002 Merge pull request #2106 from voidsatisfaction/fix-typo-on-main-app
fix tiny comment typo
2018-06-27 04:43:53 +09:00
Junyoung Choi (Sai)
4a017fd8ae Merge pull request #2066 from PeterDaveHello/patch-1
Improve Traditional Chinese translation
2018-06-27 04:43:27 +09:00
Junyoung Choi (Sai)
e6072c8fe9 Merge pull request #2131 from clone1612/updateElectron
Update Electron to v2.0.3
2018-06-27 04:41:36 +09:00
Jannick Hemelhof
f7fd99ec20 Update electron to v2.0.3 2018-06-26 11:27:37 +02:00
Junyoung Choi (Sai)
836fc13449 Merge pull request #2124 from clone1612/remove-unused
[Refactoring] - Removed unused packages
2018-06-26 06:01:30 +08:00
Junyoung Choi (Sai)
e1f78cd682 Merge pull request #2107 from ehhc/fix_images
One more try to fix the handling of images in the legacy location
2018-06-25 21:33:19 +08:00
ehhc
c69f34836a handle dot in file name 2018-06-25 15:18:45 +02:00
Junyoung Choi (Sai)
48a0db28d7 Merge pull request #2127 from BoostIO/update-issue-template
Update issue template
2018-06-25 20:22:11 +08:00
Johann Breytenbach
c36689934d Fix rendering of admonitions in Windows 2018-06-24 22:04:36 -07:00
yosmoc
653b985018 plantuml non-english character support and timing diagram support 2018-06-23 14:02:46 +09:00
ehhc
7970016fbf One more try to fix the handling of images in the legacy location 2018-06-20 08:48:55 +02:00
voidsatisfaction
129e3b283d fix tiny typo 2018-06-20 14:29:57 +09:00
Romain Le Quellec
868eefd2cf update fr translation 2018-06-19 22:20:17 +02:00
Jannick Hemelhof
68a328a364 Removed unused packages 2018-06-13 16:08:18 +02:00
Peter Dave Hello
29cd63d3a7 Improve Traditional Chinese translation 2018-06-11 18:40:55 +08:00
Nguyễn Việt Hưng
0ae1263d9d abort 2018-06-06 00:25:49 +07:00
Nguyễn Việt Hưng
31f1ebe801 removed unnecessary style 2018-06-05 21:31:50 +07:00
Nguyễn Việt Hưng
c6a9c9c57d fixed disappear scrollbar 2018-06-05 21:24:37 +07:00
Nguyễn Việt Hưng
707356bffe revert to the original function for better performance 2018-05-29 18:15:57 +07:00
Nguyễn Việt Hưng
1516807ed5 fixed double escape html 2018-05-20 19:24:36 +07:00
Nguyễn Việt Hưng
9893fd9ae5 fixed eslint 2018-05-19 19:52:47 +07:00
Nguyễn Việt Hưng
d6c28da3a8 added export escape html config 2018-05-19 19:39:08 +07:00
Nguyễn Việt Hưng
52f694a714 fixed null attachment 2018-05-19 18:58:44 +07:00
66 changed files with 2339 additions and 1083 deletions

View File

@@ -22,7 +22,9 @@
"fontSize": "14", "fontSize": "14",
"lineNumber": true "lineNumber": true
}, },
"sortBy": "UPDATED_AT", "sortBy": {
"default": "UPDATED_AT"
},
"sortTagsBy": "ALPHABETICAL", "sortTagsBy": "ALPHABETICAL",
"ui": { "ui": {
"defaultNote": "ALWAYS_ASK", "defaultNote": "ALWAYS_ASK",

View File

@@ -5,25 +5,30 @@ import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir' import 'codemirror-mode-elixir'
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
import convertModeName from 'browser/lib/convertModeName' import convertModeName from 'browser/lib/convertModeName'
import { options, TableEditor } from '@susisu/mte-kernel'
import TextEditorInterface from 'browser/lib/TextEditorInterface'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
import iconv from 'iconv-lite' import iconv from 'iconv-lite'
import crypto from 'crypto' import crypto from 'crypto'
import consts from 'browser/lib/consts' import consts from 'browser/lib/consts'
import fs from 'fs' import fs from 'fs'
const { ipcRenderer } = require('electron') const { ipcRenderer } = require('electron')
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
const buildCMRulers = (rulers, enableRulers) => const buildCMRulers = (rulers, enableRulers) =>
enableRulers ? rulers.map(ruler => ({column: ruler})) : [] (enableRulers ? rulers.map(ruler => ({ column: ruler })) : [])
export default class CodeEditor extends React.Component { export default class CodeEditor extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true}) this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
this.changeHandler = (e) => this.handleChange(e) leading: false,
trailing: true
})
this.changeHandler = e => this.handleChange(e)
this.focusHandler = () => { this.focusHandler = () => {
ipcRenderer.send('editor:focused', true) ipcRenderer.send('editor:focused', true)
} }
@@ -39,15 +44,21 @@ 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
attachmentManagement.deleteAttachmentsNotPresentInNote(this.editor.getValue(), storageKey, noteKey) attachmentManagement.deleteAttachmentsNotPresentInNote(
this.editor.getValue(),
storageKey,
noteKey
)
} }
this.pasteHandler = (editor, e) => this.handlePaste(editor, e) this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
this.loadStyleHandler = (e) => { this.loadStyleHandler = e => {
this.editor.refresh() this.editor.refresh()
} }
this.searchHandler = (e, msg) => this.handleSearch(msg) this.searchHandler = (e, msg) => this.handleSearch(msg)
this.searchState = null this.searchState = null
this.formatTable = () => this.handleFormatTable()
} }
handleSearch (msg) { handleSearch (msg) {
@@ -62,7 +73,10 @@ export default class CodeEditor extends React.Component {
cm.addOverlay(component.searchState) cm.addOverlay(component.searchState)
function makeOverlay (query, style) { function makeOverlay (query, style) {
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), 'gi') query = new RegExp(
query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'),
'gi'
)
return { return {
token: function (stream) { token: function (stream) {
query.lastIndex = stream.pos query.lastIndex = stream.pos
@@ -81,6 +95,10 @@ export default class CodeEditor extends React.Component {
}) })
} }
handleFormatTable () {
this.tableEditor.formatAll(options({textWidthOptions: {}}))
}
componentDidMount () { componentDidMount () {
const { rulers, enableRulers } = this.props const { rulers, enableRulers } = this.props
const expandSnippet = this.expandSnippet.bind(this) const expandSnippet = this.expandSnippet.bind(this)
@@ -94,7 +112,11 @@ export default class CodeEditor extends React.Component {
} }
] ]
if (!fs.existsSync(consts.SNIPPET_FILE)) { if (!fs.existsSync(consts.SNIPPET_FILE)) {
fs.writeFileSync(consts.SNIPPET_FILE, JSON.stringify(defaultSnippet, null, 4), 'utf8') fs.writeFileSync(
consts.SNIPPET_FILE,
JSON.stringify(defaultSnippet, null, 4),
'utf8'
)
} }
this.value = this.props.value this.value = this.props.value
@@ -113,7 +135,12 @@ export default class CodeEditor extends React.Component {
dragDrop: false, dragDrop: false,
foldGutter: true, foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
autoCloseBrackets: true, autoCloseBrackets: {
pairs: '()[]{}\'\'""$$**``',
triples: '```"""\'\'\'',
explode: '[]{}``$$',
override: true
},
extraKeys: { extraKeys: {
Tab: function (cm) { Tab: function (cm) {
const cursor = cm.getCursor() const cursor = cm.getCursor()
@@ -131,9 +158,14 @@ export default class CodeEditor extends React.Component {
cm.execCommand('insertSoftTab') cm.execCommand('insertSoftTab')
} }
cm.execCommand('goLineEnd') cm.execCommand('goLineEnd')
} else if (!charBeforeCursor.match(/\t|\s|\r|\n/) && cursor.ch > 1) { } else if (
!charBeforeCursor.match(/\t|\s|\r|\n/) &&
cursor.ch > 1
) {
// text expansion on tab key if the char before is alphabet // text expansion on tab key if the char before is alphabet
const snippets = JSON.parse(fs.readFileSync(consts.SNIPPET_FILE, 'utf8')) const snippets = JSON.parse(
fs.readFileSync(consts.SNIPPET_FILE, 'utf8')
)
if (expandSnippet(line, cursor, cm, snippets) === false) { if (expandSnippet(line, cursor, cm, snippets) === false) {
if (tabs) { if (tabs) {
cm.execCommand('insertTab') cm.execCommand('insertTab')
@@ -154,7 +186,7 @@ export default class CodeEditor extends React.Component {
// Do nothing // Do nothing
}, },
Enter: 'boostNewLineAndIndentContinueMarkdownList', Enter: 'boostNewLineAndIndentContinueMarkdownList',
'Ctrl-C': (cm) => { 'Ctrl-C': cm => {
if (cm.getOption('keyMap').substr(0, 3) === 'vim') { if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
document.execCommand('copy') document.execCommand('copy')
} }
@@ -182,10 +214,17 @@ export default class CodeEditor extends React.Component {
CodeMirror.Vim.defineEx('wq', 'wq', this.quitEditor) CodeMirror.Vim.defineEx('wq', 'wq', this.quitEditor)
CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor) CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor)
CodeMirror.Vim.map('ZZ', ':q', 'normal') CodeMirror.Vim.map('ZZ', ':q', 'normal')
this.tableEditor = new TableEditor(new TextEditorInterface(this.editor))
eventEmitter.on('code:format-table', this.formatTable)
} }
expandSnippet (line, cursor, cm, snippets) { expandSnippet (line, cursor, cm, snippets) {
const wordBeforeCursor = this.getWordBeforeCursor(line, cursor.line, cursor.ch) const wordBeforeCursor = this.getWordBeforeCursor(
line,
cursor.line,
cursor.ch
)
const templateCursorString = ':{}' const templateCursorString = ':{}'
for (let i = 0; i < snippets.length; i++) { for (let i = 0; i < snippets.length; i++) {
if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) { if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) {
@@ -203,7 +242,10 @@ export default class CodeEditor extends React.Component {
wordBeforeCursor.range.from, wordBeforeCursor.range.from,
wordBeforeCursor.range.to wordBeforeCursor.range.to
) )
cm.setCursor({ line: cursor.line + cursorLineNumber, ch: cursorLinePosition }) cm.setCursor({
line: cursor.line + cursorLineNumber,
ch: cursorLinePosition
})
} }
} }
} else { } else {
@@ -245,8 +287,8 @@ export default class CodeEditor extends React.Component {
return { return {
text: wordBeforeCursor, text: wordBeforeCursor,
range: { range: {
from: {line: lineNumber, ch: originCursorPosition}, from: { line: lineNumber, ch: originCursorPosition },
to: {line: lineNumber, ch: cursorPosition} to: { line: lineNumber, ch: cursorPosition }
} }
} }
} }
@@ -264,11 +306,13 @@ export default class CodeEditor extends React.Component {
this.editor.off('scroll', this.scrollHandler) this.editor.off('scroll', this.scrollHandler)
const editorTheme = document.getElementById('editorTheme') const editorTheme = document.getElementById('editorTheme')
editorTheme.removeEventListener('load', this.loadStyleHandler) editorTheme.removeEventListener('load', this.loadStyleHandler)
eventEmitter.off('code:format-table', this.formatTable)
} }
componentDidUpdate (prevProps, prevState) { componentDidUpdate (prevProps, prevState) {
let needRefresh = false let needRefresh = false
const {rulers, enableRulers} = this.props const { rulers, enableRulers } = this.props
if (prevProps.mode !== this.props.mode) { if (prevProps.mode !== this.props.mode) {
this.setMode(this.props.mode) this.setMode(this.props.mode)
} }
@@ -286,7 +330,10 @@ export default class CodeEditor extends React.Component {
needRefresh = true needRefresh = true
} }
if (prevProps.enableRulers !== enableRulers || prevProps.rulers !== rulers) { if (
prevProps.enableRulers !== enableRulers ||
prevProps.rulers !== rulers
) {
this.editor.setOption('rulers', buildCMRulers(rulers, enableRulers)) this.editor.setOption('rulers', buildCMRulers(rulers, enableRulers))
} }
@@ -326,11 +373,9 @@ export default class CodeEditor extends React.Component {
} }
} }
moveCursorTo (row, col) { moveCursorTo (row, col) {}
}
scrollToLine (num) { scrollToLine (num) {}
}
focus () { focus () {
this.editor.focus() this.editor.focus()
@@ -358,8 +403,13 @@ export default class CodeEditor extends React.Component {
handleDropImage (dropEvent) { handleDropImage (dropEvent) {
dropEvent.preventDefault() dropEvent.preventDefault()
const {storageKey, noteKey} = this.props const { storageKey, noteKey } = this.props
attachmentManagement.handleAttachmentDrop(this, storageKey, noteKey, dropEvent) attachmentManagement.handleAttachmentDrop(
this,
storageKey,
noteKey,
dropEvent
)
} }
insertAttachmentMd (imageMd) { insertAttachmentMd (imageMd) {
@@ -368,34 +418,44 @@ export default class CodeEditor extends React.Component {
handlePaste (editor, e) { handlePaste (editor, e) {
const clipboardData = e.clipboardData const clipboardData = e.clipboardData
const {storageKey, noteKey} = this.props const { storageKey, noteKey } = this.props
const dataTransferItem = clipboardData.items[0] const dataTransferItem = clipboardData.items[0]
const pastedTxt = clipboardData.getData('text') const pastedTxt = clipboardData.getData('text')
const isURL = (str) => { const isURL = str => {
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/ const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
return matcher.test(str) return matcher.test(str)
} }
const isInLinkTag = (editor) => { const isInLinkTag = editor => {
const startCursor = editor.getCursor('start') const startCursor = editor.getCursor('start')
const prevChar = editor.getRange( const prevChar = editor.getRange(
{line: startCursor.line, ch: startCursor.ch - 2}, { line: startCursor.line, ch: startCursor.ch - 2 },
{line: startCursor.line, ch: startCursor.ch} { line: startCursor.line, ch: startCursor.ch }
) )
const endCursor = editor.getCursor('end') const endCursor = editor.getCursor('end')
const nextChar = editor.getRange( const nextChar = editor.getRange(
{line: endCursor.line, ch: endCursor.ch}, { line: endCursor.line, ch: endCursor.ch },
{line: endCursor.line, ch: endCursor.ch + 1} { line: endCursor.line, ch: endCursor.ch + 1 }
) )
return prevChar === '](' && nextChar === ')' return prevChar === '](' && nextChar === ')'
} }
if (dataTransferItem.type.match('image')) { if (dataTransferItem.type.match('image')) {
attachmentManagement.handlePastImageEvent(this, storageKey, noteKey, dataTransferItem) attachmentManagement.handlePastImageEvent(
} else if (this.props.fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) { this,
storageKey,
noteKey,
dataTransferItem
)
} else if (
this.props.fetchUrlTitle &&
isURL(pastedTxt) &&
!isInLinkTag(editor)
) {
this.handlePasteUrl(e, editor, pastedTxt) this.handlePasteUrl(e, editor, pastedTxt)
} }
if (attachmentManagement.isAttachmentLink(pastedTxt)) { if (attachmentManagement.isAttachmentLink(pastedTxt)) {
attachmentManagement.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt) attachmentManagement
.then((modifiedText) => { .handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
.then(modifiedText => {
this.editor.replaceSelection(modifiedText) this.editor.replaceSelection(modifiedText)
}) })
e.preventDefault() e.preventDefault()
@@ -413,39 +473,49 @@ export default class CodeEditor extends React.Component {
const taggedUrl = `<${pastedTxt}>` const taggedUrl = `<${pastedTxt}>`
editor.replaceSelection(taggedUrl) editor.replaceSelection(taggedUrl)
const isImageReponse = (response) => { const isImageReponse = response => {
return response.headers.has('content-type') && return (
response.headers.has('content-type') &&
response.headers.get('content-type').match(/^image\/.+$/) response.headers.get('content-type').match(/^image\/.+$/)
)
} }
const replaceTaggedUrl = (replacement) => { const replaceTaggedUrl = replacement => {
const value = editor.getValue() const value = editor.getValue()
const cursor = editor.getCursor() const cursor = editor.getCursor()
const newValue = value.replace(taggedUrl, replacement) const newValue = value.replace(taggedUrl, replacement)
const newCursor = Object.assign({}, cursor, { ch: cursor.ch + newValue.length - value.length }) const newCursor = Object.assign({}, cursor, {
ch: cursor.ch + newValue.length - value.length
})
editor.setValue(newValue) editor.setValue(newValue)
editor.setCursor(newCursor) editor.setCursor(newCursor)
} }
fetch(pastedTxt, { fetch(pastedTxt, {
method: 'get' method: 'get'
}).then((response) => {
if (isImageReponse(response)) {
return this.mapImageResponse(response, pastedTxt)
} else {
return this.mapNormalResponse(response, pastedTxt)
}
}).then((replacement) => {
replaceTaggedUrl(replacement)
}).catch((e) => {
replaceTaggedUrl(pastedTxt)
}) })
.then(response => {
if (isImageReponse(response)) {
return this.mapImageResponse(response, pastedTxt)
} else {
return this.mapNormalResponse(response, pastedTxt)
}
})
.then(replacement => {
replaceTaggedUrl(replacement)
})
.catch(e => {
replaceTaggedUrl(pastedTxt)
})
} }
mapNormalResponse (response, pastedTxt) { mapNormalResponse (response, pastedTxt) {
return this.decodeResponse(response).then((body) => { return this.decodeResponse(response).then(body => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const parsedBody = (new window.DOMParser()).parseFromString(body, 'text/html') const parsedBody = new window.DOMParser().parseFromString(
body,
'text/html'
)
const linkWithTitle = `[${parsedBody.title}](${pastedTxt})` const linkWithTitle = `[${parsedBody.title}](${pastedTxt})`
resolve(linkWithTitle) resolve(linkWithTitle)
} catch (e) { } catch (e) {
@@ -473,10 +543,13 @@ export default class CodeEditor extends React.Component {
const _charset = headers.has('content-type') const _charset = headers.has('content-type')
? this.extractContentTypeCharset(headers.get('content-type')) ? this.extractContentTypeCharset(headers.get('content-type'))
: undefined : undefined
return response.arrayBuffer().then((buff) => { return response.arrayBuffer().then(buff => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const charset = _charset !== undefined && iconv.encodingExists(_charset) ? _charset : 'utf-8' const charset = _charset !== undefined &&
iconv.encodingExists(_charset)
? _charset
: 'utf-8'
resolve(iconv.decode(new Buffer(buff), charset).toString()) resolve(iconv.decode(new Buffer(buff), charset).toString())
} catch (e) { } catch (e) {
reject(e) reject(e)
@@ -486,34 +559,31 @@ export default class CodeEditor extends React.Component {
} }
extractContentTypeCharset (contentType) { extractContentTypeCharset (contentType) {
return contentType.split(';').filter((str) => { return contentType
return str.trim().toLowerCase().startsWith('charset') .split(';')
}).map((str) => { .filter(str => {
return str.replace(/['"]/g, '').split('=')[1] return str.trim().toLowerCase().startsWith('charset')
})[0] })
.map(str => {
return str.replace(/['"]/g, '').split('=')[1]
})[0]
} }
render () { render () {
const {className, fontSize} = this.props const {className, fontSize} = this.props
let fontFamily = this.props.fontFamily const fontFamily = normalizeEditorFontFamily(this.props.fontFamily)
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
? [fontFamily].concat(defaultEditorFontFamily)
: defaultEditorFontFamily
const width = this.props.width const width = this.props.width
return ( return (
<div <div
className={className == null className={className == null ? 'CodeEditor' : `CodeEditor ${className}`}
? 'CodeEditor'
: `CodeEditor ${className}`
}
ref='root' ref='root'
tabIndex='-1' tabIndex='-1'
style={{ style={{
fontFamily: fontFamily.join(', '), fontFamily,
fontSize: fontSize, fontSize: fontSize,
width: width width: width
}} }}
onDrop={(e) => this.handleDropImage(e)} onDrop={e => this.handleDropImage(e)}
/> />
) )
} }

View File

@@ -7,7 +7,9 @@ import 'codemirror-mode-elixir'
import consts from 'browser/lib/consts' import consts from 'browser/lib/consts'
import Raphael from 'raphael' import Raphael from 'raphael'
import flowchart from 'flowchart' import flowchart from 'flowchart'
import mermaidRender from './render/MermaidRender'
import SequenceDiagram from 'js-sequence-diagrams' import SequenceDiagram from 'js-sequence-diagrams'
import Chart from 'chart.js'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
import htmlTextHelper from 'browser/lib/htmlTextHelper' import htmlTextHelper from 'browser/lib/htmlTextHelper'
import convertModeName from 'browser/lib/convertModeName' import convertModeName from 'browser/lib/convertModeName'
@@ -21,18 +23,29 @@ const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
const { app } = remote const { app } = remote
const path = require('path') 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 markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const appPath = 'file://' + (process.env.NODE_ENV === 'production' const appPath = fileUrl(
? app.getAppPath() process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
: path.resolve()) )
const CSS_FILES = [ const CSS_FILES = [
`${appPath}/node_modules/katex/dist/katex.min.css`, `${appPath}/node_modules/katex/dist/katex.min.css`,
`${appPath}/node_modules/codemirror/lib/codemirror.css` `${appPath}/node_modules/codemirror/lib/codemirror.css`
] ]
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS) { function buildStyle (
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
) {
return ` return `
@font-face { @font-face {
font-family: 'Lato'; font-family: 'Lato';
@@ -129,6 +142,25 @@ body p {
` `
} }
const scrollBarStyle = `
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.15);
}
`
const scrollBarDarkStyle = `
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.3);
}
`
const { shell } = require('electron') const { shell } = require('electron')
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
@@ -137,17 +169,27 @@ if (!OSX) {
defaultFontFamily.unshift('Microsoft YaHei') defaultFontFamily.unshift('Microsoft YaHei')
defaultFontFamily.unshift('meiryo') defaultFontFamily.unshift('meiryo')
} }
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace'] const defaultCodeBlockFontFamily = [
'Monaco',
'Menlo',
'Ubuntu Mono',
'Consolas',
'source-code-pro',
'monospace'
]
export default class MarkdownPreview extends React.Component { export default class MarkdownPreview extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.contextMenuHandler = (e) => this.handleContextMenu(e) this.contextMenuHandler = e => this.handleContextMenu(e)
this.mouseDownHandler = (e) => this.handleMouseDown(e) this.mouseDownHandler = e => this.handleMouseDown(e)
this.mouseUpHandler = (e) => this.handleMouseUp(e) this.mouseUpHandler = e => this.handleMouseUp(e)
this.DoubleClickHandler = (e) => this.handleDoubleClick(e) this.DoubleClickHandler = e => this.handleDoubleClick(e)
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true}) this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e) leading: false,
trailing: true
})
this.checkboxClickHandler = e => this.handleCheckboxClick(e)
this.saveAsTextHandler = () => this.handleSaveAsText() this.saveAsTextHandler = () => this.handleSaveAsText()
this.saveAsMdHandler = () => this.handleSaveAsMd() this.saveAsMdHandler = () => this.handleSaveAsMd()
this.saveAsHtmlHandler = () => this.handleSaveAsHtml() this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
@@ -209,36 +251,85 @@ export default class MarkdownPreview extends React.Component {
} }
handleSaveAsMd () { handleSaveAsMd () {
this.exportAsDocument('md') this.exportAsDocument('md', (noteContent, exportTasks) => {
let result = noteContent
if (this.props && this.props.storagePath && this.props.noteKey) {
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
this.props.storagePath
)
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER
})
})
result = attachmentManagement.removeStorageAndNoteReferences(
noteContent,
this.props.noteKey
)
}
return result
})
} }
handleSaveAsHtml () { handleSaveAsHtml () {
this.exportAsDocument('html', (noteContent, exportTasks) => { this.exportAsDocument('html', (noteContent, exportTasks) => {
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams() const {
fontFamily,
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS) fontSize,
let body = this.markdown.render(escapeHtmlCharacters(noteContent)) codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = this.getStyleParams()
const inlineStyles = buildStyle(
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
)
let body = this.markdown.render(
escapeHtmlCharacters(noteContent, { detectCodeBlock: true })
)
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(noteContent, this.props.storagePath) const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
this.props.storagePath
)
files.forEach((file) => { files.forEach(file => {
file = file.replace('file://', '') if (global.process.platform === 'win32') {
file = file.replace('file:///', '')
} else {
file = file.replace('file://', '')
}
exportTasks.push({ exportTasks.push({
src: file, src: file,
dst: 'css' dst: 'css'
}) })
}) })
attachmentsAbsolutePaths.forEach((attachment) => { attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({ exportTasks.push({
src: attachment, src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER dst: attachmentManagement.DESTINATION_FOLDER
}) })
}) })
body = attachmentManagement.removeStorageAndNoteReferences(body, this.props.noteKey) body = attachmentManagement.removeStorageAndNoteReferences(
body,
this.props.noteKey
)
let styles = '' let styles = ''
files.forEach((file) => { files.forEach(file => {
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">` styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
}) })
@@ -260,50 +351,75 @@ export default class MarkdownPreview extends React.Component {
exportAsDocument (fileType, contentFormatter) { exportAsDocument (fileType, contentFormatter) {
const options = { const options = {
filters: [ filters: [{ name: 'Documents', extensions: [fileType] }],
{name: 'Documents', extensions: [fileType]}
],
properties: ['openFile', 'createDirectory'] properties: ['openFile', 'createDirectory']
} }
dialog.showSaveDialog(remote.getCurrentWindow(), options, dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
(filename) => { if (filename) {
if (filename) { const content = this.props.value
const content = this.props.value const storage = this.props.storagePath
const storage = this.props.storagePath
exportNote(storage, content, filename, contentFormatter) exportNote(storage, content, filename, contentFormatter)
.then((res) => { .then(res => {
dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`}) dialog.showMessageBox(remote.getCurrentWindow(), {
}).catch((err) => { type: 'info',
dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export') message: `Exported to ${filename}`
throw err })
}) })
} .catch(err => {
}) dialog.showErrorBox(
'Export error',
err ? err.message || err : 'Unexpected error during export'
)
throw err
})
}
})
} }
fixDecodedURI (node) { fixDecodedURI (node) {
if (node && node.children.length === 1 && typeof node.children[0] === 'string') { if (
node &&
node.children.length === 1 &&
typeof node.children[0] === 'string'
) {
const { innerText, href } = node const { innerText, href } = node
node.innerText = mdurl.decode(href) === innerText node.innerText = mdurl.decode(href) === innerText ? href : innerText
? href }
: innerText }
getScrollBarStyle () {
const { theme } = this.props
switch (theme) {
case 'dark':
case 'solarized-dark':
case 'monokai':
return scrollBarDarkStyle
default:
return scrollBarStyle
} }
} }
componentDidMount () { componentDidMount () {
this.refs.root.setAttribute('sandbox', 'allow-scripts') this.refs.root.setAttribute('sandbox', 'allow-scripts')
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler) this.refs.root.contentWindow.document.body.addEventListener(
'contextmenu',
this.contextMenuHandler
)
let styles = ` let styles = `
<style id='style'></style> <style id='style'></style>
<link rel="stylesheet" id="codeTheme"> <link rel="stylesheet" id="codeTheme">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
${this.getScrollBarStyle()}
</style>
` `
CSS_FILES.forEach((file) => { CSS_FILES.forEach(file => {
styles += `<link rel="stylesheet" href="${file}">` styles += `<link rel="stylesheet" href="${file}">`
}) })
@@ -311,12 +427,30 @@ export default class MarkdownPreview extends React.Component {
this.rewriteIframe() this.rewriteIframe()
this.applyStyle() this.applyStyle()
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler) this.refs.root.contentWindow.document.addEventListener(
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler) 'mousedown',
this.refs.root.contentWindow.document.addEventListener('dblclick', this.DoubleClickHandler) this.mouseDownHandler
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler) )
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler) this.refs.root.contentWindow.document.addEventListener(
this.refs.root.contentWindow.document.addEventListener('scroll', this.scrollHandler) 'mouseup',
this.mouseUpHandler
)
this.refs.root.contentWindow.document.addEventListener(
'dblclick',
this.DoubleClickHandler
)
this.refs.root.contentWindow.document.addEventListener(
'drop',
this.preventImageDroppedHandler
)
this.refs.root.contentWindow.document.addEventListener(
'dragover',
this.preventImageDroppedHandler
)
this.refs.root.contentWindow.document.addEventListener(
'scroll',
this.scrollHandler
)
eventEmitter.on('export:save-text', this.saveAsTextHandler) eventEmitter.on('export:save-text', this.saveAsTextHandler)
eventEmitter.on('export:save-md', this.saveAsMdHandler) eventEmitter.on('export:save-md', this.saveAsMdHandler)
eventEmitter.on('export:save-html', this.saveAsHtmlHandler) eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
@@ -324,13 +458,34 @@ export default class MarkdownPreview extends React.Component {
} }
componentWillUnmount () { componentWillUnmount () {
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler) this.refs.root.contentWindow.document.body.removeEventListener(
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler) 'contextmenu',
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler) this.contextMenuHandler
this.refs.root.contentWindow.document.removeEventListener('dblclick', this.DoubleClickHandler) )
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler) this.refs.root.contentWindow.document.removeEventListener(
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler) 'mousedown',
this.refs.root.contentWindow.document.removeEventListener('scroll', this.scrollHandler) this.mouseDownHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'mouseup',
this.mouseUpHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'dblclick',
this.DoubleClickHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'drop',
this.preventImageDroppedHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'dragover',
this.preventImageDroppedHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'scroll',
this.scrollHandler
)
eventEmitter.off('export:save-text', this.saveAsTextHandler) eventEmitter.off('export:save-text', this.saveAsTextHandler)
eventEmitter.off('export:save-md', this.saveAsMdHandler) eventEmitter.off('export:save-md', this.saveAsMdHandler)
eventEmitter.off('export:save-html', this.saveAsHtmlHandler) eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
@@ -339,14 +494,17 @@ export default class MarkdownPreview extends React.Component {
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value) this.rewriteIframe() if (prevProps.value !== this.props.value) this.rewriteIframe()
if (prevProps.smartQuotes !== this.props.smartQuotes || if (
prevProps.sanitize !== this.props.sanitize || prevProps.smartQuotes !== this.props.smartQuotes ||
prevProps.smartArrows !== this.props.smartArrows || prevProps.sanitize !== this.props.sanitize ||
prevProps.breaks !== this.props.breaks) { prevProps.smartArrows !== this.props.smartArrows ||
prevProps.breaks !== this.props.breaks
) {
this.initMarkdown() this.initMarkdown()
this.rewriteIframe() this.rewriteIframe()
} }
if (prevProps.fontFamily !== this.props.fontFamily || if (
prevProps.fontFamily !== this.props.fontFamily ||
prevProps.fontSize !== this.props.fontSize || prevProps.fontSize !== this.props.fontSize ||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily || prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
prevProps.codeBlockTheme !== this.props.codeBlockTheme || prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
@@ -355,34 +513,82 @@ export default class MarkdownPreview extends React.Component {
prevProps.theme !== this.props.theme || prevProps.theme !== this.props.theme ||
prevProps.scrollPastEnd !== this.props.scrollPastEnd || prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
prevProps.allowCustomCSS !== this.props.allowCustomCSS || prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
prevProps.customCSS !== this.props.customCSS) { prevProps.customCSS !== this.props.customCSS
) {
this.applyStyle() this.applyStyle()
this.rewriteIframe() this.rewriteIframe()
} }
} }
getStyleParams () { getStyleParams () {
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS } = this.props const {
fontSize,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = this.props
let { fontFamily, codeBlockFontFamily } = this.props let { fontFamily, codeBlockFontFamily } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0 fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily) ? fontFamily
: defaultFontFamily .split(',')
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0 .map(fontName => fontName.trim())
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily) .concat(defaultFontFamily)
: defaultCodeBlockFontFamily : 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} return {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
}
} }
applyStyle () { applyStyle () {
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams() const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = this.getStyleParams()
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme) this.getWindow().document.getElementById(
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS) 'codeTheme'
).href = this.GetCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
)
} }
GetCodeThemeLink (theme) { GetCodeThemeLink (theme) {
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default' theme = consts.THEMES.some(_theme => _theme === theme) &&
theme !== 'default'
? theme ? theme
: 'elegant' : 'elegant'
return theme.startsWith('solarized') return theme.startsWith('solarized')
@@ -391,71 +597,92 @@ export default class MarkdownPreview extends React.Component {
} }
rewriteIframe () { rewriteIframe () {
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => { _.forEach(
el.removeEventListener('click', this.checkboxClickHandler) this.refs.root.contentWindow.document.querySelectorAll(
}) 'input[type="checkbox"]'
),
el => {
el.removeEventListener('click', this.checkboxClickHandler)
}
)
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { _.forEach(
el.removeEventListener('click', this.linkClickHandler) this.refs.root.contentWindow.document.querySelectorAll('a'),
}) el => {
el.removeEventListener('click', this.linkClickHandler)
}
)
const { theme, indentSize, showCopyNotification, storagePath, noteKey } = this.props const {
theme,
indentSize,
showCopyNotification,
storagePath,
noteKey
} = this.props
let { value, codeBlockTheme } = this.props let { value, codeBlockTheme } = this.props
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme) this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
const renderedHTML = this.markdown.render(value)
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(
renderedHTML,
storagePath
)
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll(
'input[type="checkbox"]'
),
el => {
el.addEventListener('click', this.checkboxClickHandler)
}
)
const codeBlocks = value.match(/(```)(.|[\n])*?(```)/g) _.forEach(
if (codeBlocks !== null) { this.refs.root.contentWindow.document.querySelectorAll('a'),
codeBlocks.forEach((codeBlock) => { el => {
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock)) this.fixDecodedURI(el)
}) el.addEventListener('click', this.linkClickHandler)
} }
let renderedHTML = this.markdown.render(value) )
attachmentManagement.migrateAttachments(renderedHTML, storagePath, noteKey)
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath)
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => { codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme)
el.addEventListener('click', this.checkboxClickHandler)
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
this.fixDecodedURI(el)
el.addEventListener('click', this.linkClickHandler)
})
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
? codeBlockTheme ? codeBlockTheme
: 'default' : 'default'
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.code code'), (el) => { _.forEach(
let syntax = CodeMirror.findModeByName(convertModeName(el.className)) this.refs.root.contentWindow.document.querySelectorAll('.code code'),
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') el => {
CodeMirror.requireMode(syntax.mode, () => { let syntax = CodeMirror.findModeByName(convertModeName(el.className))
const content = htmlTextHelper.decodeEntities(el.innerHTML) if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
const copyIcon = document.createElement('i') CodeMirror.requireMode(syntax.mode, () => {
copyIcon.innerHTML = '<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>' const content = htmlTextHelper.decodeEntities(el.innerHTML)
copyIcon.onclick = (e) => { const copyIcon = document.createElement('i')
copy(content) copyIcon.innerHTML =
if (showCopyNotification) { '<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
this.notify('Saved to Clipboard!', { copyIcon.onclick = e => {
body: 'Paste it wherever you want!', copy(content)
silent: true if (showCopyNotification) {
}) this.notify('Saved to Clipboard!', {
body: 'Paste it wherever you want!',
silent: true
})
}
} }
} el.parentNode.appendChild(copyIcon)
el.parentNode.appendChild(copyIcon) el.innerHTML = ''
el.innerHTML = '' if (codeBlockTheme.indexOf('solarized') === 0) {
if (codeBlockTheme.indexOf('solarized') === 0) { const [refThema, color] = codeBlockTheme.split(' ')
const [refThema, color] = codeBlockTheme.split(' ') el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}` } else {
} else { el.parentNode.className += ` cm-s-${codeBlockTheme}`
el.parentNode.className += ` cm-s-${codeBlockTheme}` }
} CodeMirror.runMode(content, syntax.mime, el, {
CodeMirror.runMode(content, syntax.mime, el, { tabSize: indentSize
tabSize: indentSize })
}) })
}) }
}) )
const opts = {} const opts = {}
// if (this.props.theme === 'dark') { // if (this.props.theme === 'dark') {
// opts['font-color'] = '#DDD' // opts['font-color'] = '#DDD'
@@ -463,37 +690,71 @@ export default class MarkdownPreview extends React.Component {
// opts['element-color'] = '#DDD' // opts['element-color'] = '#DDD'
// opts['fill'] = '#3A404C' // opts['fill'] = '#3A404C'
// } // }
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), (el) => { _.forEach(
Raphael.setWindow(this.getWindow()) this.refs.root.contentWindow.document.querySelectorAll('.flowchart'),
try { el => {
const diagram = flowchart.parse(htmlTextHelper.decodeEntities(el.innerHTML)) Raphael.setWindow(this.getWindow())
el.innerHTML = '' try {
diagram.drawSVG(el, opts) const diagram = flowchart.parse(
_.forEach(el.querySelectorAll('a'), (el) => { htmlTextHelper.decodeEntities(el.innerHTML)
el.addEventListener('click', this.linkClickHandler) )
}) el.innerHTML = ''
} catch (e) { diagram.drawSVG(el, opts)
console.error(e) _.forEach(el.querySelectorAll('a'), el => {
el.className = 'flowchart-error' el.addEventListener('click', this.linkClickHandler)
el.innerHTML = 'Flowchart parse error: ' + e.message })
} catch (e) {
console.error(e)
el.className = 'flowchart-error'
el.innerHTML = 'Flowchart parse error: ' + e.message
}
} }
}) )
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.sequence'), (el) => { _.forEach(
Raphael.setWindow(this.getWindow()) this.refs.root.contentWindow.document.querySelectorAll('.sequence'),
try { el => {
const diagram = SequenceDiagram.parse(htmlTextHelper.decodeEntities(el.innerHTML)) Raphael.setWindow(this.getWindow())
el.innerHTML = '' try {
diagram.drawSVG(el, {theme: 'simple'}) const diagram = SequenceDiagram.parse(
_.forEach(el.querySelectorAll('a'), (el) => { htmlTextHelper.decodeEntities(el.innerHTML)
el.addEventListener('click', this.linkClickHandler) )
}) el.innerHTML = ''
} catch (e) { diagram.drawSVG(el, { theme: 'simple' })
console.error(e) _.forEach(el.querySelectorAll('a'), el => {
el.className = 'sequence-error' el.addEventListener('click', this.linkClickHandler)
el.innerHTML = 'Sequence diagram parse error: ' + e.message })
} catch (e) {
console.error(e)
el.className = 'sequence-error'
el.innerHTML = 'Sequence diagram parse error: ' + e.message
}
} }
}) )
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.chart'),
el => {
try {
const chartConfig = JSON.parse(el.innerHTML)
el.innerHTML = ''
var canvas = document.createElement('canvas')
el.appendChild(canvas)
/* eslint-disable no-new */
new Chart(canvas, chartConfig)
} catch (e) {
console.error(e)
el.className = 'chart-error'
el.innerHTML = 'chartjs diagram parse error: ' + e.message
}
}
)
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
el => {
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme)
}
)
} }
focus () { focus () {
@@ -505,7 +766,9 @@ export default class MarkdownPreview extends React.Component {
} }
scrollTo (targetRow) { scrollTo (targetRow) {
const blocks = this.getWindow().document.querySelectorAll('body>[data-line]') const blocks = this.getWindow().document.querySelectorAll(
'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]
@@ -525,7 +788,11 @@ export default class MarkdownPreview extends React.Component {
notify (title, options) { notify (title, options) {
if (global.process.platform === 'win32') { if (global.process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png') options.icon = path.join(
'file://',
global.__dirname,
'../../resources/app.png'
)
} }
return new window.Notification(title, options) return new window.Notification(title, options)
} }
@@ -540,7 +807,9 @@ export default class MarkdownPreview extends React.Component {
const regexNoteInternalLink = /main.html#(.+)/ const regexNoteInternalLink = /main.html#(.+)/
if (regexNoteInternalLink.test(linkHash)) { if (regexNoteInternalLink.test(linkHash)) {
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1]) const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
const targetElement = this.refs.root.contentWindow.document.getElementById(targetId) const targetElement = this.refs.root.contentWindow.document.getElementById(
targetId
)
if (targetElement != null) { if (targetElement != null) {
this.getWindow().scrollTo(0, targetElement.offsetTop) this.getWindow().scrollTo(0, targetElement.offsetTop)
@@ -574,9 +843,9 @@ export default class MarkdownPreview extends React.Component {
render () { render () {
const { className, style, tabIndex } = this.props const { className, style, tabIndex } = this.props
return ( return (
<iframe className={className != null <iframe
? 'MarkdownPreview ' + className className={
: 'MarkdownPreview' className != null ? 'MarkdownPreview ' + className : 'MarkdownPreview'
} }
style={style} style={style}
tabIndex={tabIndex} tabIndex={tabIndex}

View File

@@ -26,14 +26,12 @@ const TagElement = ({ tagName }) => (
* @param {Array|null} tags * @param {Array|null} tags
* @return {React.Component} * @return {React.Component}
*/ */
const TagElementList = (tags) => { const TagElementList = tags => {
if (!isArray(tags)) { if (!isArray(tags)) {
return [] return []
} }
const tagElements = tags.map(tag => ( const tagElements = tags.map(tag => TagElement({ tagName: tag }))
TagElement({tagName: tag})
))
return tagElements return tagElements
} }
@@ -59,10 +57,8 @@ const NoteItem = ({
folderName, folderName,
viewType viewType
}) => ( }) => (
<div styleName={isActive <div
? 'item--active' styleName={isActive ? 'item--active' : 'item'}
: 'item'
}
key={note.key} key={note.key}
onClick={e => handleNoteClick(e, note.key)} onClick={e => handleNoteClick(e, note.key)}
onContextMenu={e => handleNoteContextMenu(e, note.key)} onContextMenu={e => handleNoteContextMenu(e, note.key)}
@@ -72,42 +68,54 @@ const NoteItem = ({
<div styleName='item-wrapper'> <div styleName='item-wrapper'>
{note.type === 'SNIPPET_NOTE' {note.type === 'SNIPPET_NOTE'
? <i styleName='item-title-icon' className='fa fa-fw fa-code' /> ? <i styleName='item-title-icon' className='fa fa-fw fa-code' />
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' /> : <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />}
}
<div styleName='item-title'> <div styleName='item-title'>
{note.title.trim().length > 0 {note.title.trim().length > 0
? note.title ? note.title
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span> : <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>}
}
</div> </div>
{['ALL', 'STORAGE'].includes(viewType) && <div styleName='item-middle'> {['ALL', 'STORAGE'].includes(viewType) &&
<div styleName='item-middle-time'>{dateDisplay}</div> <div styleName='item-middle'>
<div styleName='item-middle-app-meta'> <div styleName='item-middle-time'>{dateDisplay}</div>
<div title={viewType === 'ALL' ? storageName : viewType === 'STORAGE' ? folderName : null} styleName='item-middle-app-meta-label'> <div styleName='item-middle-app-meta'>
{viewType === 'ALL' && storageName} <div
{viewType === 'STORAGE' && folderName} title={
viewType === 'ALL'
? storageName
: viewType === 'STORAGE' ? folderName : null
}
styleName='item-middle-app-meta-label'
>
{viewType === 'ALL' && storageName}
{viewType === 'STORAGE' && folderName}
</div>
</div> </div>
</div> </div>}
</div>}
<div styleName='item-bottom'> <div styleName='item-bottom'>
<div styleName='item-bottom-tagList'> <div styleName='item-bottom-tagList'>
{note.tags.length > 0 {note.tags.length > 0
? TagElementList(note.tags) ? TagElementList(note.tags)
: <span style={{ fontStyle: 'italic', opacity: 0.5 }} styleName='item-bottom-tagList-empty'>{i18n.__('No tags')}</span> : <span
} style={{ fontStyle: 'italic', opacity: 0.5 }}
styleName='item-bottom-tagList-empty'
>
{i18n.__('No tags')}
</span>}
</div> </div>
<div> <div>
{note.isStarred {note.isStarred
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : '' ? <img
} styleName='item-star'
src='../resources/icon/icon-starred.svg'
/>
: ''}
{note.isPinned && !pathname.match(/\/starred|\/trash/) {note.isPinned && !pathname.match(/\/starred|\/trash/)
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : '' ? <i styleName='item-pin' className='fa fa-thumb-tack' />
} : ''}
{note.type === 'MARKDOWN_NOTE' {note.type === 'MARKDOWN_NOTE'
? <TodoProcess todoStatus={getTodoStatus(note.content)} /> ? <TodoProcess todoStatus={getTodoStatus(note.content)} />
: '' : ''}
}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -54,9 +54,8 @@ const StorageItem = ({
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
> >
{!isFolded && ( {!isFolded &&
<DraggableIcon className={styles['folderList-item-reorder']} /> <DraggableIcon className={styles['folderList-item-reorder']} />}
)}
<span <span
styleName={ styleName={
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name' isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
@@ -72,12 +71,10 @@ const StorageItem = ({
: folderName} : folderName}
</span> </span>
{!isFolded && {!isFolded &&
_.isNumber(noteCount) && ( _.isNumber(noteCount) &&
<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'>{folderName}</span>
)}
</button> </button>
) )
} }

View File

@@ -68,7 +68,7 @@ body
padding 5px padding 5px
margin -5px margin -5px
border-radius 5px border-radius 5px
.flowchart-error, .sequence-error .flowchart-error, .sequence-error .chart-error
background-color errorBackgroundColor background-color errorBackgroundColor
color errorTextColor color errorTextColor
padding 5px padding 5px
@@ -213,7 +213,7 @@ pre
margin 0 0 1em margin 0 0 1em
display flex display flex
line-height 1.4em line-height 1.4em
&.flowchart, &.sequence &.flowchart, &.sequence, &.chart
display flex display flex
justify-content center justify-content center
background-color white background-color white
@@ -315,6 +315,8 @@ $admonition-icon
position absolute position absolute
left 1.2rem left 1.2rem
font-family: "Material Icons" font-family: "Material Icons"
font-weight: normal;
font-style: normal;
font-size: 24px font-size: 24px
display: inline-block; display: inline-block;
line-height: 1; line-height: 1;
@@ -346,27 +348,27 @@ $admonition-title
margin-bottom 0 margin-bottom 0
admonition_types = { admonition_types = {
note: {border-color: #448aff, title-color: rgba(68,138,255,.1), icon: "note"}, note: {color: #0288D1, icon: "note"},
hint: {border-color: #00bfa5, title-color: rgba(0,191,165,.1), icon: "info"}, hint: {color: #009688, icon: "info_outline"},
danger: {border-color: #ff1744, title-color: rgba(255,23,68,.1), icon: "block"}, danger: {color: #c2185b, icon: "block"},
caution: {border-color: #ff9100, title-color: rgba(255,145,0,.1), icon: "warning"}, caution: {color: #ffa726, icon: "warning"},
error: {border-color: #ff1744, title-color: rgba(255,23,68,.1), icon: "error"}, error: {color: #d32f2f, icon: "error_outline"},
attention: {border-color: #64dd17, title-color: rgba(100,221,23,.1), icon: "priority_high"} attention: {color: #455a64, icon: "priority_high"}
} }
for name, val in admonition_types for name, val in admonition_types
.admonition.{name} .admonition.{name}
@extend $admonition @extend $admonition
border-left-color: val[border-color] border-left-color: val[color]
.admonition.{name}>.admonition-title .admonition.{name}>.admonition-title
@extend $admonition-title @extend $admonition-title
border-bottom-color: .1rem solid val[title-color] border-bottom-color: .1rem solid rgba(val[color], 0.2)
background-color: val[title-color] background-color: rgba(val[color], 0.2)
.admonition.{name}>.admonition-title:before .admonition.{name}>.admonition-title:before
@extend $admonition-icon @extend $admonition-icon
color: val[border-color] color: val[color]
content: val[icon] content: val[icon]
themeDarkBackground = darken(#21252B, 10%) themeDarkBackground = darken(#21252B, 10%)

View File

@@ -0,0 +1,39 @@
import mermaidAPI from 'mermaid'
// fixes bad styling in the mermaid dark theme
const darkThemeStyling = `
.loopText tspan {
fill: white;
}`
function getRandomInt (min, max) {
return Math.floor(Math.random() * (max - min)) + min
}
function getId () {
var pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
var id = 'm-'
for (var i = 0; i < 7; i++) {
id += pool[getRandomInt(0, 16)]
}
return id
}
function render (element, content, theme) {
try {
let isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai'
mermaidAPI.initialize({
theme: isDarkTheme ? 'dark' : 'default',
themeCSS: isDarkTheme ? darkThemeStyling : ''
})
mermaidAPI.render(getId(), content, (svgGraph) => {
element.innerHTML = svgGraph
})
} catch (e) {
console.error(e)
element.className = 'mermaid-error'
element.innerHTML = 'mermaid diagram parse error: ' + e.message
}
}
export default render

View File

@@ -0,0 +1,53 @@
import { Point } from '@susisu/mte-kernel'
export default class TextEditorInterface {
constructor (editor) {
this.editor = editor
}
getCursorPosition () {
const pos = this.editor.getCursor()
return new Point(pos.line, pos.ch)
}
setCursorPosition (pos) {
this.editor.setCursor({line: pos.row, ch: pos.column})
}
setSelectionRange (range) {
this.editor.setSelection({
anchor: {line: range.start.row, ch: range.start.column},
head: {line: range.end.row, ch: range.end.column}
})
}
getLastRow () {
return this.editor.lastLine()
}
acceptsTableEdit (row) {
return true
}
getLine (row) {
return this.editor.getLine(row)
}
insertLine (row, line) {
this.editor.replaceRange(line, {line: row, ch: 0})
}
deleteLine (row) {
this.editor.replaceRange('', {line: row, ch: 0}, {line: row, ch: this.editor.getLine(row).length})
}
replaceLines (startRow, endRow, lines) {
endRow-- // because endRow is a first line after a table.
const endRowCh = this.editor.getLine(endRow).length
this.editor.replaceRange(lines, {line: startRow, ch: 0}, {line: endRow, ch: endRowCh})
}
transact (func) {
func()
}
}

View File

@@ -36,7 +36,15 @@ const consts = {
'Violet Eggplant' 'Violet Eggplant'
], ],
THEMES: ['default'].concat(themes), THEMES: ['default'].concat(themes),
SNIPPET_FILE: snippetFile SNIPPET_FILE: snippetFile,
DEFAULT_EDITOR_FONT_FAMILY: [
'Monaco',
'Menlo',
'Ubuntu Mono',
'Consolas',
'source-code-pro',
'monospace'
]
} }
module.exports = consts module.exports = consts

View File

@@ -5,10 +5,10 @@ export function getTodoStatus (content) {
splitted.forEach((line) => { splitted.forEach((line) => {
const trimmedLine = line.trim() const trimmedLine = line.trim()
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./)) { if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./i)) {
numberOfTodo++ numberOfTodo++
} }
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) { if (trimmedLine.match(/^[\+\-\*] \[x\] ./i)) {
numberOfCompletedTodo++ numberOfCompletedTodo++
} }
}) })

View File

@@ -1,6 +1,7 @@
'use strict' 'use strict'
import sanitizeHtml from 'sanitize-html' import sanitizeHtml from 'sanitize-html'
import { escapeHtmlCharacters } from './utils'
module.exports = function sanitizePlugin (md, options) { module.exports = function sanitizePlugin (md, options) {
options = options || {} options = options || {}
@@ -8,13 +9,26 @@ module.exports = function sanitizePlugin (md, options) {
md.core.ruler.after('linkify', 'sanitize_inline', state => { md.core.ruler.after('linkify', 'sanitize_inline', state => {
for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) { for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) {
if (state.tokens[tokenIdx].type === 'html_block') { if (state.tokens[tokenIdx].type === 'html_block') {
state.tokens[tokenIdx].content = sanitizeHtml(state.tokens[tokenIdx].content, options) state.tokens[tokenIdx].content = sanitizeHtml(
state.tokens[tokenIdx].content,
options
)
}
if (state.tokens[tokenIdx].type === 'fence') {
// escapeHtmlCharacters has better performance
state.tokens[tokenIdx].content = escapeHtmlCharacters(
state.tokens[tokenIdx].content,
{ skipSingleQuote: true }
)
} }
if (state.tokens[tokenIdx].type === 'inline') { if (state.tokens[tokenIdx].type === 'inline') {
const inlineTokens = state.tokens[tokenIdx].children const inlineTokens = state.tokens[tokenIdx].children
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) { for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
if (inlineTokens[childIdx].type === 'html_inline') { if (inlineTokens[childIdx].type === 'html_inline') {
inlineTokens[childIdx].content = sanitizeHtml(inlineTokens[childIdx].content, options) inlineTokens[childIdx].content = sanitizeHtml(
inlineTokens[childIdx].content,
options
)
} }
} }
} }

View File

@@ -40,6 +40,12 @@ class Markdown {
if (langType === 'sequence') { if (langType === 'sequence') {
return `<pre class="sequence">${str}</pre>` return `<pre class="sequence">${str}</pre>`
} }
if (langType === 'chart') {
return `<pre class="chart">${str}</pre>`
}
if (langType === 'mermaid') {
return `<pre class="mermaid">${str}</pre>`
}
return '<pre class="code CodeMirror">' + return '<pre class="code CodeMirror">' +
'<span class="filename">' + fileName + '</span>' + '<span class="filename">' + fileName + '</span>' +
createGutter(str, firstLineNumber) + createGutter(str, firstLineNumber) +
@@ -157,6 +163,22 @@ class Markdown {
} }
}) })
// Ditaa support
this.md.use(require('markdown-it-plantuml'), {
openMarker: '@startditaa',
closeMarker: '@endditaa',
generateSource: function (umlCode) {
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
// Currently PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/png'
const s = unescape(encodeURIComponent(umlCode))
const zippedCode = deflate.encode64(
deflate.zip_deflate(`@startditaa\n${s}\n@endditaa`, 9)
)
return `${serverAddress}/${zippedCode}`
}
})
// Override task item // Override task item
this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) { this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
let content, terminate, i, l, token let content, terminate, i, l, token
@@ -245,4 +267,3 @@ class Markdown {
} }
export default Markdown export default Markdown

View File

@@ -0,0 +1,9 @@
import consts from 'browser/lib/consts'
import isString from 'lodash/isString'
export default function normalizeEditorFontFamily (fontFamily) {
const defaultEditorFontFamily = consts.DEFAULT_EDITOR_FONT_FAMILY
return isString(fontFamily) && fontFamily.length > 0
? [fontFamily].concat(defaultEditorFontFamily).join(', ')
: defaultEditorFontFamily.join(', ')
}

View File

@@ -23,7 +23,9 @@ function findByWordOrTag (notes, block) {
return true return true
} }
if (note.type === 'SNIPPET_NOTE') { if (note.type === 'SNIPPET_NOTE') {
return note.description.match(wordRegExp) return note.description.match(wordRegExp) || note.snippets.some((snippet) => {
return snippet.name.match(wordRegExp) || snippet.content.match(wordRegExp)
})
} else if (note.type === 'MARKDOWN_NOTE') { } else if (note.type === 'MARKDOWN_NOTE') {
return note.content.match(wordRegExp) return note.content.match(wordRegExp)
} }

View File

@@ -6,52 +6,113 @@ export function lastFindInArray (array, callback) {
} }
} }
export function escapeHtmlCharacters (text) { export function escapeHtmlCharacters (
const matchHtmlRegExp = /["'&<>]/ html,
const str = '' + text opt = { detectCodeBlock: false, skipSingleQuote: false }
const match = matchHtmlRegExp.exec(str) ) {
const matchHtmlRegExp = /["'&<>]/g
const matchCodeBlockRegExp = /```/g
const escapes = ['&quot;', '&amp;', '&#39;', '&lt;', '&gt;']
let match = null
const replaceAt = (str, index, replace) =>
str.substr(0, index) +
replace +
str.substr(index + replace.length - (replace.length - 1))
if (!match) { while ((match = matchHtmlRegExp.exec(html)) !== null) {
return str const current = { char: match[0], index: match.index }
} const codeBlockIndexs = []
let openCodeBlock = null
let escape // if the detectCodeBlock option is activated then this function should skip
let html = '' // characters that needed to be escape but located in code block
let index = 0 if (opt.detectCodeBlock) {
let lastIndex = 0 // The first type of code block is lines that start with 4 spaces
// Here we check for the \n character located before the character that
for (index = match.index; index < str.length; index++) { // needed to be escape. It means we check for the begining of the line that
switch (str.charCodeAt(index)) { // contain that character, then we check if there are 4 spaces next to the
case 34: // " // \n character (the line start with 4 spaces)
escape = '&quot;' let previousLineEnd = current.index - 1
break while (html[previousLineEnd] !== '\n' && previousLineEnd !== -1) {
case 38: // & previousLineEnd--
escape = '&amp;' }
break // 4 spaces means this character is in a code block
case 39: // ' if (
escape = '&#39;' html[previousLineEnd + 1] === ' ' &&
break html[previousLineEnd + 2] === ' ' &&
case 60: // < html[previousLineEnd + 3] === ' ' &&
escape = '&lt;' html[previousLineEnd + 4] === ' '
break ) {
case 62: // > // skip the current character
escape = '&gt;'
break
default:
continue continue
}
// The second type of code block is lines that wrapped in ```
// We will get the position of each ```
// then push it into an array
// then the array returned will be like this:
// [startCodeblock, endCodeBlock, startCodeBlock, endCodeBlock]
while ((openCodeBlock = matchCodeBlockRegExp.exec(html)) !== null) {
codeBlockIndexs.push(openCodeBlock.index)
}
let shouldSkipChar = false
// we loop through the array of positions
// we skip 2 element as the i index position is the position of ``` that
// open the codeblock and the i + 1 is the position of the ``` that close
// the code block
for (let i = 0; i < codeBlockIndexs.length; i += 2) {
// the i index position is the position of the ``` that open code block
// so we have to + 2 as that position is the position of the first ` in the ````
// but we need to make sure that the position current character is larger
// that the last ` in the ``` that open the code block so we have to take
// the position of the first ` and + 2
// the i + 1 index position is the closing ``` so the char must less than it
if (
current.index > codeBlockIndexs[i] + 2 &&
current.index < codeBlockIndexs[i + 1]
) {
// skip it
shouldSkipChar = true
break
}
}
if (shouldSkipChar) {
// skip the current character
continue
}
} }
// otherwise, escape it !!!
if (lastIndex !== index) { if (current.char === '&') {
html += str.substring(lastIndex, index) // when escaping character & we have to be becareful as the & could be a part
// of an escaped character like &quot; will be came &amp;quot;
let nextStr = ''
let nextIndex = current.index
let escapedStr = false
// maximum length of an escaped string is 5. For example ('&quot;')
// we take the next 5 character of the next string if it is one of the string:
// ['&quot;', '&amp;', '&#39;', '&lt;', '&gt;'] then we will not escape the & character
// as it is a part of the escaped string and should not be escaped
while (nextStr.length <= 5) {
nextStr += html[nextIndex]
nextIndex++
if (escapes.indexOf(nextStr) !== -1) {
escapedStr = true
break
}
}
if (!escapedStr) {
// this & char is not a part of an escaped string
html = replaceAt(html, current.index, '&amp;')
}
} else if (current.char === '"') {
html = replaceAt(html, current.index, '&quot;')
} else if (current.char === "'" && !opt.skipSingleQuote) {
html = replaceAt(html, current.index, '&#39;')
} else if (current.char === '<') {
html = replaceAt(html, current.index, '&lt;')
} else if (current.char === '>') {
html = replaceAt(html, current.index, '&gt;')
} }
lastIndex = index + 1
html += escape
} }
return html
return lastIndex !== index
? html + str.substring(lastIndex, index)
: html
} }
export function isObjectEqual (a, b) { export function isObjectEqual (a, b) {

View File

@@ -4,12 +4,14 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './FullscreenButton.styl' import styles from './FullscreenButton.styl'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin'
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
const FullscreenButton = ({ const FullscreenButton = ({
onClick onClick
}) => ( }) => (
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}> <button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' /> <img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span> <span styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
</button> </button>
) )

View File

@@ -33,6 +33,7 @@
.control-infoButton-panel-trash .control-infoButton-panel-trash
z-index 200 z-index 200
margin-top 0px margin-top 0px
top 50px
right 0px right 0px
position absolute position absolute
padding 20px 25px 0 25px padding 20px 25px 0 25px

View File

@@ -32,7 +32,7 @@ import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
const electron = require('electron') const electron = require('electron')
const { remote } = electron const { remote } = electron
const { Menu, MenuItem, dialog } = remote const { dialog } = remote
class SnippetNoteDetail extends React.Component { class SnippetNoteDetail extends React.Component {
constructor (props) { constructor (props) {
@@ -441,7 +441,7 @@ class SnippetNoteDetail extends React.Component {
const isSuper = global.process.platform === 'darwin' const isSuper = global.process.platform === 'darwin'
? e.metaKey ? e.metaKey
: e.ctrlKey : e.ctrlKey
if (isSuper) { if (isSuper && !e.shiftKey) {
e.preventDefault() e.preventDefault()
this.addSnippet() this.addSnippet()
} }
@@ -451,14 +451,14 @@ class SnippetNoteDetail extends React.Component {
} }
handleModeButtonClick (e, index) { handleModeButtonClick (e, index) {
const menu = new Menu() const templetes = []
CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => { CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => {
menu.append(new MenuItem({ templetes.push({
label: mode.name, label: mode.name,
click: (e) => this.handleModeOptionClick(index, mode.name)(e) click: (e) => this.handleModeOptionClick(index, mode.name)(e)
})) })
}) })
menu.popup(remote.getCurrentWindow()) context.popup(templetes)
} }
handleIndentTypeButtonClick (e) { handleIndentTypeButtonClick (e) {

View File

@@ -5,6 +5,7 @@ import styles from './TagSelect.styl'
import _ from 'lodash' import _ from 'lodash'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import ee from 'browser/main/lib/eventEmitter'
class TagSelect extends React.Component { class TagSelect extends React.Component {
constructor (props) { constructor (props) {
@@ -13,16 +14,26 @@ class TagSelect extends React.Component {
this.state = { this.state = {
newTag: '' newTag: ''
} }
this.addtagHandler = this.handleAddTag.bind(this)
} }
componentDidMount () { componentDidMount () {
this.value = this.props.value this.value = this.props.value
ee.on('editor:add-tag', this.addtagHandler)
} }
componentDidUpdate () { componentDidUpdate () {
this.value = this.props.value this.value = this.props.value
} }
componentWillUnmount () {
ee.off('editor:add-tag', this.addtagHandler)
}
handleAddTag () {
this.refs.newTag.focus()
}
handleNewTagInputKeyDown (e) { handleNewTagInputKeyDown (e) {
switch (e.keyCode) { switch (e.keyCode) {
case 9: case 9:

View File

@@ -9,6 +9,7 @@ import ee from 'browser/main/lib/eventEmitter'
import StatusBar from '../StatusBar' import StatusBar from '../StatusBar'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import debounceRender from 'react-debounce-render' import debounceRender from 'react-debounce-render'
import searchFromNotes from 'browser/lib/search'
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
@@ -35,11 +36,38 @@ class Detail extends React.Component {
} }
render () { render () {
const { location, data, config } = this.props const { location, data, params, config } = this.props
let note = null let note = null
if (location.query.key != null) { if (location.query.key != null) {
const noteKey = location.query.key const noteKey = location.query.key
note = data.noteMap.get(noteKey) const allNotes = data.noteMap.map(note => note)
const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey))
let displayedNotes = allNotes
if (location.pathname.match(/\/searched/)) {
const searchStr = params.searchword
displayedNotes = searchStr === undefined || searchStr === '' ? allNotes
: searchFromNotes(allNotes, searchStr)
}
if (location.pathname.match(/\/tags/)) {
const listOfTags = params.tagname.split(' ')
displayedNotes = data.noteMap.map(note => note).filter(note =>
listOfTags.every(tag => note.tags.includes(tag))
)
}
if (location.pathname.match(/\/trashed/)) {
displayedNotes = trashedNotes
} else {
displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key)
}
const noteKeys = displayedNotes.map(note => note.key)
if (noteKeys.includes(noteKey)) {
note = data.noteMap.get(noteKey)
}
} }
if (note == null) { if (note == null) {

View File

@@ -22,7 +22,6 @@ const electron = require('electron')
const { remote } = electron const { remote } = electron
class Main extends React.Component { class Main extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
@@ -60,10 +59,10 @@ class Main extends React.Component {
name: 'My Storage', name: 'My Storage',
path: path.join(remote.app.getPath('home'), 'Boostnote') path: path.join(remote.app.getPath('home'), 'Boostnote')
}) })
.then((data) => { .then(data => {
return data return data
}) })
.then((data) => { .then(data => {
if (data.storage.folders[0] != null) { if (data.storage.folders[0] != null) {
return data return data
} else { } else {
@@ -72,7 +71,7 @@ class Main extends React.Component {
color: '#1278BD', color: '#1278BD',
name: 'Default' name: 'Default'
}) })
.then((_data) => { .then(_data => {
return { return {
storage: _data.storage, storage: _data.storage,
notes: data.notes notes: data.notes
@@ -80,7 +79,7 @@ class Main extends React.Component {
}) })
} }
}) })
.then((data) => { .then(data => {
console.log(data) console.log(data)
store.dispatch({ store.dispatch({
type: 'ADD_STORAGE', type: 'ADD_STORAGE',
@@ -98,16 +97,16 @@ class Main extends React.Component {
{ {
name: 'example.html', name: 'example.html',
mode: 'html', mode: 'html',
content: '<html>\n<body>\n<h1 id=\'hello\'>Enjoy Boostnote!</h1>\n</body>\n</html>' content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>"
}, },
{ {
name: 'example.js', name: 'example.js',
mode: 'javascript', mode: 'javascript',
content: 'var boostnote = document.getElementById(\'enjoy\').innerHTML\n\nconsole.log(boostnote)' content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)"
} }
] ]
}) })
.then((note) => { .then(note => {
store.dispatch({ store.dispatch({
type: 'UPDATE_NOTE', type: 'UPDATE_NOTE',
note: note note: note
@@ -120,7 +119,7 @@ class Main extends React.Component {
title: 'Welcome to Boostnote!', title: 'Welcome to Boostnote!',
content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)' content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
}) })
.then((note) => { .then(note => {
store.dispatch({ store.dispatch({
type: 'UPDATE_NOTE', type: 'UPDATE_NOTE',
note: note note: note
@@ -131,10 +130,10 @@ class Main extends React.Component {
.then(defaultMarkdownNote) .then(defaultMarkdownNote)
.then(() => data.storage) .then(() => data.storage)
}) })
.then((storage) => { .then(storage => {
hashHistory.push('/storages/' + storage.key) hashHistory.push('/storages/' + storage.key)
}) })
.catch((err) => { .catch(err => {
throw err throw err
}) })
} }
@@ -142,12 +141,7 @@ class Main extends React.Component {
componentDidMount () { componentDidMount () {
const { dispatch, config } = this.props const { dispatch, config } = this.props
const supportedThemes = [ const supportedThemes = ['dark', 'white', 'solarized-dark', 'monokai']
'dark',
'white',
'solarized-dark',
'monokai'
]
if (supportedThemes.indexOf(config.ui.theme) !== -1) { if (supportedThemes.indexOf(config.ui.theme) !== -1) {
document.body.setAttribute('data-theme', config.ui.theme) document.body.setAttribute('data-theme', config.ui.theme)
@@ -162,19 +156,18 @@ class Main extends React.Component {
} }
applyShortcuts() applyShortcuts()
// Reload all data // Reload all data
dataApi.init() dataApi.init().then(data => {
.then((data) => { dispatch({
dispatch({ type: 'INIT_ALL',
type: 'INIT_ALL', storages: data.storages,
storages: data.storages, notes: data.notes
notes: data.notes
})
if (data.storages.length < 1) {
this.init()
}
}) })
if (data.storages.length < 1) {
this.init()
}
})
eventEmitter.on('editor:fullscreen', this.toggleFullScreen) eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
} }
@@ -199,34 +192,40 @@ class Main extends React.Component {
handleMouseUp (e) { handleMouseUp (e) {
// Change width of NoteList component. // Change width of NoteList component.
if (this.state.isRightSliderFocused) { if (this.state.isRightSliderFocused) {
this.setState({ this.setState(
isRightSliderFocused: false {
}, () => { isRightSliderFocused: false
const { dispatch } = this.props },
const newListWidth = this.state.listWidth () => {
// TODO: ConfigManager should dispatch itself. const { dispatch } = this.props
ConfigManager.set({listWidth: newListWidth}) const newListWidth = this.state.listWidth
dispatch({ // TODO: ConfigManager should dispatch itself.
type: 'SET_LIST_WIDTH', ConfigManager.set({ listWidth: newListWidth })
listWidth: newListWidth dispatch({
}) type: 'SET_LIST_WIDTH',
}) listWidth: newListWidth
})
}
)
} }
// Change width of SideNav component. // Change width of SideNav component.
if (this.state.isLeftSliderFocused) { if (this.state.isLeftSliderFocused) {
this.setState({ this.setState(
isLeftSliderFocused: false {
}, () => { isLeftSliderFocused: false
const { dispatch } = this.props },
const navWidth = this.state.navWidth () => {
// TODO: ConfigManager should dispatch itself. const { dispatch } = this.props
ConfigManager.set({ navWidth }) const navWidth = this.state.navWidth
dispatch({ // TODO: ConfigManager should dispatch itself.
type: 'SET_NAV_WIDTH', ConfigManager.set({ navWidth })
navWidth dispatch({
}) type: 'SET_NAV_WIDTH',
}) navWidth
})
}
)
} }
} }
@@ -271,8 +270,8 @@ class Main extends React.Component {
} }
hideLeftLists (noteDetail, noteList, mainBody) { hideLeftLists (noteDetail, noteList, mainBody) {
this.setState({noteDetailWidth: noteDetail.style.left}) this.setState({ noteDetailWidth: noteDetail.style.left })
this.setState({mainBodyWidth: mainBody.style.left}) this.setState({ mainBodyWidth: mainBody.style.left })
noteDetail.style.left = '0px' noteDetail.style.left = '0px'
mainBody.style.left = '0px' mainBody.style.left = '0px'
noteList.style.display = 'none' noteList.style.display = 'none'
@@ -294,33 +293,36 @@ class Main extends React.Component {
<div <div
className='Main' className='Main'
styleName='root' styleName='root'
onMouseMove={(e) => this.handleMouseMove(e)} onMouseMove={e => this.handleMouseMove(e)}
onMouseUp={(e) => this.handleMouseUp(e)} onMouseUp={e => this.handleMouseUp(e)}
> >
<SideNav <SideNav
{..._.pick(this.props, [ {..._.pick(this.props, ['dispatch', 'data', 'config', 'location'])}
'dispatch',
'data',
'config',
'location'
])}
width={this.state.navWidth} width={this.state.navWidth}
/> />
{!config.isSideNavFolded && {!config.isSideNavFolded &&
<div styleName={this.state.isLeftSliderFocused ? 'slider--active' : 'slider'} <div
style={{left: this.state.navWidth}} styleName={
onMouseDown={(e) => this.handleLeftSlideMouseDown(e)} this.state.isLeftSliderFocused ? 'slider--active' : 'slider'
}
style={{ left: this.state.navWidth }}
onMouseDown={e => this.handleLeftSlideMouseDown(e)}
draggable='false' draggable='false'
> >
<div styleName='slider-hitbox' /> <div styleName='slider-hitbox' />
</div> </div>}
} <div
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'} styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
id='main-body' id='main-body'
ref='body' ref='body'
style={{left: config.isSideNavFolded ? foldedNavigationWidth : this.state.navWidth}} style={{
left: config.isSideNavFolded
? foldedNavigationWidth
: this.state.navWidth
}}
> >
<TopBar style={{width: this.state.listWidth}} <TopBar
style={{ width: this.state.listWidth }}
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
'config', 'config',
@@ -329,7 +331,8 @@ class Main extends React.Component {
'location' 'location'
])} ])}
/> />
<NoteList style={{width: this.state.listWidth}} <NoteList
style={{ width: this.state.listWidth }}
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
'data', 'data',
@@ -338,15 +341,20 @@ class Main extends React.Component {
'location' 'location'
])} ])}
/> />
<div styleName={this.state.isRightSliderFocused ? 'slider-right--active' : 'slider-right'} <div
style={{left: this.state.listWidth - 1}} styleName={
onMouseDown={(e) => this.handleRightSlideMouseDown(e)} this.state.isRightSliderFocused
? 'slider-right--active'
: 'slider-right'
}
style={{ left: this.state.listWidth - 1 }}
onMouseDown={e => this.handleRightSlideMouseDown(e)}
draggable='false' draggable='false'
> >
<div styleName='slider-hitbox' /> <div styleName='slider-hitbox' />
</div> </div>
<Detail <Detail
style={{left: this.state.listWidth}} style={{ left: this.state.listWidth }}
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
'data', 'data',
@@ -374,4 +382,4 @@ Main.propTypes = {
data: PropTypes.shape({}).isRequired data: PropTypes.shape({}).isRequired
} }
export default connect((x) => x)(CSSModules(Main, styles)) export default connect(x => x)(CSSModules(Main, styles))

View File

@@ -21,9 +21,10 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import Markdown from '../../lib/markdown' 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'
const { remote } = require('electron') const { remote } = require('electron')
const { Menu, MenuItem, dialog } = remote const { dialog } = remote
const WP_POST_PATH = '/wp/v2/posts' const WP_POST_PATH = '/wp/v2/posts'
function sortByCreatedAt (a, b) { function sortByCreatedAt (a, b) {
@@ -417,10 +418,10 @@ class NoteList extends React.Component {
} }
handleSortByChange (e) { handleSortByChange (e) {
const { dispatch } = this.props const { dispatch, params: { folderKey } } = this.props
const config = { const config = {
sortBy: e.target.value [folderKey]: { sortBy: e.target.value }
} }
ConfigManager.set(config) ConfigManager.set(config)
@@ -491,55 +492,51 @@ class NoteList extends React.Component {
const updateLabel = i18n.__('Update Blog') const updateLabel = i18n.__('Update Blog')
const openBlogLabel = i18n.__('Open Blog') const openBlogLabel = i18n.__('Open Blog')
const menu = new Menu() const templates = []
if (location.pathname.match(/\/trash/)) { if (location.pathname.match(/\/trash/)) {
menu.append(new MenuItem({ templates.push({
label: restoreNote, label: restoreNote,
click: this.restoreNote click: this.restoreNote
})) }, {
menu.append(new MenuItem({
label: deleteLabel, label: deleteLabel,
click: this.deleteNote click: this.deleteNote
})) })
} else { } else {
if (!location.pathname.match(/\/starred/)) { if (!location.pathname.match(/\/starred/)) {
menu.append(new MenuItem({ templates.push({
label: pinLabel, label: pinLabel,
click: this.pinToTop click: this.pinToTop
})) })
} }
menu.append(new MenuItem({ templates.push({
label: deleteLabel, label: deleteLabel,
click: this.deleteNote click: this.deleteNote
})) }, {
menu.append(new MenuItem({
label: cloneNote, label: cloneNote,
click: this.cloneNote.bind(this) click: this.cloneNote.bind(this)
})) }, {
menu.append(new MenuItem({
label: copyNoteLink, label: copyNoteLink,
click: this.copyNoteLink(note) click: this.copyNoteLink(note)
})) })
if (note.type === 'MARKDOWN_NOTE') { if (note.type === 'MARKDOWN_NOTE') {
if (note.blog && note.blog.blogLink && note.blog.blogId) { if (note.blog && note.blog.blogLink && note.blog.blogId) {
menu.append(new MenuItem({ templates.push({
label: updateLabel, label: updateLabel,
click: this.publishMarkdown.bind(this) click: this.publishMarkdown.bind(this)
})) }, {
menu.append(new MenuItem({
label: openBlogLabel, label: openBlogLabel,
click: () => this.openBlog.bind(this)(note) click: () => this.openBlog.bind(this)(note)
})) })
} else { } else {
menu.append(new MenuItem({ templates.push({
label: publishLabel, label: publishLabel,
click: this.publishMarkdown.bind(this) click: this.publishMarkdown.bind(this)
})) })
} }
} }
} }
menu.popup() context.popup(templates)
} }
updateSelectedNotes (updateFunc, cleanSelection = true) { updateSelectedNotes (updateFunc, cleanSelection = true) {
@@ -912,12 +909,13 @@ class NoteList extends React.Component {
} }
render () { render () {
const { location, config } = this.props const { location, config, params: { folderKey } } = this.props
let { notes } = this.props let { notes } = this.props
const { selectedNoteKeys } = this.state const { selectedNoteKeys } = this.state
const sortFunc = config.sortBy === 'CREATED_AT' const sortBy = _.get(config, [folderKey, 'sortBy'], config.sortBy.default)
const sortFunc = sortBy === 'CREATED_AT'
? sortByCreatedAt ? sortByCreatedAt
: config.sortBy === 'ALPHABETICAL' : sortBy === 'ALPHABETICAL'
? sortByAlphabetical ? sortByAlphabetical
: sortByUpdatedAt : sortByUpdatedAt
const sortedNotes = location.pathname.match(/\/starred|\/trash/) const sortedNotes = location.pathname.match(/\/starred|\/trash/)
@@ -968,7 +966,7 @@ class NoteList extends React.Component {
notes.length === 1 || notes.length === 1 ||
(autoSelectFirst && index === 0) (autoSelectFirst && index === 0)
const dateDisplay = moment( const dateDisplay = moment(
config.sortBy === 'CREATED_AT' sortBy === 'CREATED_AT'
? note.createdAt : note.updatedAt ? note.createdAt : note.updatedAt
).fromNow('D') ).fromNow('D')
@@ -1017,7 +1015,7 @@ class NoteList extends React.Component {
<i className='fa fa-angle-down' /> <i className='fa fa-angle-down' />
<select styleName='control-sortBy-select' <select styleName='control-sortBy-select'
title={i18n.__('Select filter mode')} title={i18n.__('Select filter mode')}
value={config.sortBy} value={sortBy}
onChange={(e) => this.handleSortByChange(e)} onChange={(e) => this.handleSortByChange(e)}
> >
<option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option> <option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option>

View File

@@ -11,9 +11,10 @@ import StorageItemChild from 'browser/components/StorageItem'
import _ from 'lodash' import _ from 'lodash'
import { SortableElement } from 'react-sortable-hoc' import { SortableElement } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
const { remote } = require('electron') const { remote } = require('electron')
const { Menu, dialog } = remote const { dialog } = remote
const escapeStringRegexp = require('escape-string-regexp') const escapeStringRegexp = require('escape-string-regexp')
const path = require('path') const path = require('path')
@@ -21,13 +22,15 @@ class StorageItem extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
const { storage } = this.props
this.state = { this.state = {
isOpen: true isOpen: !!storage.isOpen
} }
} }
handleHeaderContextMenu (e) { handleHeaderContextMenu (e) {
const menu = Menu.buildFromTemplate([ context.popup([
{ {
label: i18n.__('Add Folder'), label: i18n.__('Add Folder'),
click: (e) => this.handleAddFolderButtonClick(e) click: (e) => this.handleAddFolderButtonClick(e)
@@ -40,8 +43,6 @@ class StorageItem extends React.Component {
click: (e) => this.handleUnlinkStorageClick(e) click: (e) => this.handleUnlinkStorageClick(e)
} }
]) ])
menu.popup()
} }
handleUnlinkStorageClick (e) { handleUnlinkStorageClick (e) {
@@ -68,8 +69,18 @@ class StorageItem extends React.Component {
} }
handleToggleButtonClick (e) { handleToggleButtonClick (e) {
const { storage, dispatch } = this.props
const isOpen = !this.state.isOpen
dataApi.toggleStorage(storage.key, isOpen)
.then((storage) => {
dispatch({
type: 'EXPAND_STORAGE',
storage,
isOpen
})
})
this.setState({ this.setState({
isOpen: !this.state.isOpen isOpen: isOpen
}) })
} }
@@ -94,7 +105,7 @@ class StorageItem extends React.Component {
} }
handleFolderButtonContextMenu (e, folder) { handleFolderButtonContextMenu (e, folder) {
const menu = Menu.buildFromTemplate([ context.popup([
{ {
label: i18n.__('Rename Folder'), label: i18n.__('Rename Folder'),
click: (e) => this.handleRenameFolderClick(e, folder) click: (e) => this.handleRenameFolderClick(e, folder)
@@ -123,8 +134,6 @@ class StorageItem extends React.Component {
click: (e) => this.handleFolderDeleteClick(e, folder) click: (e) => this.handleFolderDeleteClick(e, folder)
} }
]) ])
menu.popup()
} }
handleRenameFolderClick (e, folder) { handleRenameFolderClick (e, folder) {

View File

@@ -1,8 +1,6 @@
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'
const { remote } = require('electron')
const { Menu } = remote
import dataApi from 'browser/main/lib/dataApi' 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'
@@ -19,6 +17,7 @@ import ListButton from './ListButton'
import TagButton from './TagButton' import TagButton from './TagButton'
import {SortableContainer} from 'react-sortable-hoc' import {SortableContainer} from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
class SideNav extends React.Component { class SideNav extends React.Component {
// TODO: should not use electron stuff v0.7 // TODO: should not use electron stuff v0.7
@@ -254,10 +253,9 @@ class SideNav extends React.Component {
handleFilterButtonContextMenu (event) { handleFilterButtonContextMenu (event) {
const { data } = this.props const { data } = this.props
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey)) const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
const menu = Menu.buildFromTemplate([ context.popup([
{ label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) } { label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) }
]) ])
menu.popup()
} }
render () { render () {

View File

@@ -4,10 +4,11 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './StatusBar.styl' import styles from './StatusBar.styl'
import ZoomManager from 'browser/main/lib/ZoomManager' import ZoomManager from 'browser/main/lib/ZoomManager'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
const electron = require('electron') const electron = require('electron')
const { remote, ipcRenderer } = electron const { remote, ipcRenderer } = electron
const { Menu, MenuItem, dialog } = remote const { dialog } = remote
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0] const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
@@ -26,16 +27,16 @@ class StatusBar extends React.Component {
} }
handleZoomButtonClick (e) { handleZoomButtonClick (e) {
const menu = new Menu() const templates = []
zoomOptions.forEach((zoom) => { zoomOptions.forEach((zoom) => {
menu.append(new MenuItem({ templates.push({
label: Math.floor(zoom * 100) + '%', label: Math.floor(zoom * 100) + '%',
click: () => this.handleZoomMenuItemClick(zoom) click: () => this.handleZoomMenuItemClick(zoom)
})) })
}) })
menu.popup(remote.getCurrentWindow()) context.popup(templates)
} }
handleZoomMenuItemClick (zoomFactor) { handleZoomMenuItemClick (zoomFactor) {

View File

@@ -156,8 +156,7 @@ class TopBar extends React.Component {
if (this.state.isSearching) { if (this.state.isSearching) {
el.blur() el.blur()
} else { } else {
el.focus() el.select()
el.setSelectionRange(0, el.value.length)
} }
} }

View File

@@ -15,6 +15,12 @@ body
font-weight 200 font-weight 200
-webkit-font-smoothing antialiased -webkit-font-smoothing antialiased
::-webkit-scrollbar
width 12px
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.15)
button, input, select, textarea button, input, select, textarea
font-family DEFAULT_FONTS font-family DEFAULT_FONTS
@@ -85,9 +91,11 @@ modalBackColor = white
absolute top left bottom right absolute top left bottom right
background-color modalBackColor background-color modalBackColor
z-index modalZIndex + 1 z-index modalZIndex + 1
body[data-theme="dark"] body[data-theme="dark"]
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase .ModalBase
.modalBack .modalBack
background-color $ui-dark-backgroundColor background-color $ui-dark-backgroundColor
@@ -128,6 +136,8 @@ body[data-theme="dark"]
z-index modalZIndex + 5 z-index modalZIndex + 5
body[data-theme="solarized-dark"] body[data-theme="solarized-dark"]
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase .ModalBase
.modalBack .modalBack
background-color $ui-solarized-dark-backgroundColor background-color $ui-solarized-dark-backgroundColor
@@ -135,9 +145,14 @@ body[data-theme="solarized-dark"]
color: $ui-solarized-dark-text-color color: $ui-solarized-dark-text-color
body[data-theme="monokai"] body[data-theme="monokai"]
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase .ModalBase
.modalBack .modalBack
background-color $ui-monokai-backgroundColor background-color $ui-monokai-backgroundColor
.sortableItemHelper .sortableItemHelper
color: $ui-monokai-text-color color: $ui-monokai-text-color
body[data-theme="default"]
.SideNav ::-webkit-scrollbar-thumb
background-color rgba(255, 255, 255, 0.3)

View File

@@ -16,7 +16,9 @@ export const DEFAULT_CONFIG = {
isSideNavFolded: false, isSideNavFolded: false,
listWidth: 280, listWidth: 280,
navWidth: 200, navWidth: 200,
sortBy: 'UPDATED_AT', // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL' sortBy: {
default: 'UPDATED_AT' // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL'
},
sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER' sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER'
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL' listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
amaEnabled: true, amaEnabled: true,

View File

@@ -37,7 +37,8 @@ function addStorage (input) {
key, key,
name: input.name, name: input.name,
type: input.type, type: input.type,
path: input.path path: input.path,
isOpen: false
} }
return Promise.resolve(newStorage) return Promise.resolve(newStorage)
@@ -48,7 +49,8 @@ function addStorage (input) {
key: newStorage.key, key: newStorage.key,
type: newStorage.type, type: newStorage.type,
name: newStorage.name, name: newStorage.name,
path: newStorage.path path: newStorage.path,
isOpen: false
}) })
localStorage.setItem('storages', JSON.stringify(rawStorages)) localStorage.setItem('storages', JSON.stringify(rawStorages))

View File

@@ -10,6 +10,7 @@ import i18n from 'browser/lib/i18n'
const STORAGE_FOLDER_PLACEHOLDER = ':storage' const STORAGE_FOLDER_PLACEHOLDER = ':storage'
const DESTINATION_FOLDER = 'attachments' const DESTINATION_FOLDER = 'attachments'
const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(path.win32.sep)
/** /**
* @description * @description
@@ -76,14 +77,14 @@ function createAttachmentDestinationFolder (destinationStoragePath, noteKey) {
/** /**
* @description Moves attachments from the old location ('/images') to the new one ('/attachments/noteKey) * @description Moves attachments from the old location ('/images') to the new one ('/attachments/noteKey)
* @param renderedHTML HTML of the current note * @param markdownContent of the current note
* @param storagePath Storage path of the current note * @param storagePath Storage path of the current note
* @param noteKey Key of the current note * @param noteKey Key of the current note
*/ */
function migrateAttachments (renderedHTML, storagePath, noteKey) { function migrateAttachments (markdownContent, storagePath, noteKey) {
if (sander.existsSync(path.join(storagePath, 'images'))) { if (noteKey !== undefined && sander.existsSync(path.join(storagePath, 'images'))) {
const attachments = getAttachmentsInContent(renderedHTML) || [] const attachments = getAttachmentsInMarkdownContent(markdownContent) || []
if (attachments !== []) { if (attachments.length) {
createAttachmentDestinationFolder(storagePath, noteKey) createAttachmentDestinationFolder(storagePath, noteKey)
} }
for (const attachment of attachments) { for (const attachment of attachments) {
@@ -106,7 +107,10 @@ function migrateAttachments (renderedHTML, storagePath, noteKey) {
* @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths. * @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
*/ */
function fixLocalURLS (renderedHTML, storagePath) { function fixLocalURLS (renderedHTML, storagePath) {
return renderedHTML.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER)) return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?"', 'g'), function (match) {
var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g')
return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
})
} }
/** /**
@@ -186,13 +190,13 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
} }
/** /**
* @description Returns all attachment paths of the given markdown * @description Returns all attachment paths of the given markdown
* @param {String} markdownContent content in which the attachment paths should be found * @param {String} markdownContent content in which the attachment paths should be found
* @returns {String[]} Array of the relative paths (starting with :storage) of the attachments of the given markdown * @returns {String[]} Array of the relative paths (starting with :storage) of the attachments of the given markdown
*/ */
function getAttachmentsInContent (markdownContent) { function getAttachmentsInMarkdownContent (markdownContent) {
const preparedInput = markdownContent.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep) const preparedInput = markdownContent.replace(new RegExp('[' + PATH_SEPARATORS + ']', 'g'), path.sep)
const regexp = new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + '?([a-zA-Z0-9]|-)*' + escapeStringRegexp(path.sep) + '[a-zA-Z0-9]+(\\.[a-zA-Z0-9]+)?', 'g') const regexp = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + ')' + '?([a-zA-Z0-9]|-)*' + '(' + escapeStringRegexp(path.sep) + ')' + '([a-zA-Z0-9]|\\.)+(\\.[a-zA-Z0-9]+)?', 'g')
return preparedInput.match(regexp) return preparedInput.match(regexp)
} }
@@ -203,7 +207,7 @@ function getAttachmentsInContent (markdownContent) {
* @returns {String[]} Absolute paths of the referenced attachments * @returns {String[]} Absolute paths of the referenced attachments
*/ */
function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) { function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) {
const temp = getAttachmentsInContent(markdownContent) || [] const temp = getAttachmentsInMarkdownContent(markdownContent) || []
const result = [] const result = []
for (const relativePath of temp) { for (const relativePath of temp) {
result.push(relativePath.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER))) result.push(relativePath.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER)))
@@ -239,7 +243,8 @@ function moveAttachments (oldPath, newPath, noteKey, newNoteKey, noteContent) {
*/ */
function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) { function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) {
if (noteContent) { if (noteContent) {
return noteContent.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey, 'g'), path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey)) const preparedInput = noteContent.replace(new RegExp('[' + PATH_SEPARATORS + ']', 'g'), path.sep)
return preparedInput.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey, 'g'), path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey))
} }
return noteContent return noteContent
} }
@@ -277,7 +282,7 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey
} }
const targetStorage = findStorage.findStorage(storageKey) const targetStorage = findStorage.findStorage(storageKey)
const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
const attachmentsInNote = getAttachmentsInContent(markdownContent) const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent)
const attachmentsInNoteOnlyFileNames = [] const attachmentsInNoteOnlyFileNames = []
if (attachmentsInNote) { if (attachmentsInNote) {
for (let i = 0; i < attachmentsInNote.length; i++) { for (let i = 0; i < attachmentsInNote.length; i++) {
@@ -348,7 +353,7 @@ function generateFileNotFoundMarkdown () {
*/ */
function isAttachmentLink (text) { function isAttachmentLink (text) {
if (text) { if (text) {
return text.match(new RegExp('.*\\[.*\\]\\( *' + escapeStringRegexp(STORAGE_FOLDER_PLACEHOLDER) + escapeStringRegexp(path.sep) + '.*\\).*', 'gi')) != null return text.match(new RegExp('.*\\[.*\\]\\( *' + escapeStringRegexp(STORAGE_FOLDER_PLACEHOLDER) + '[' + PATH_SEPARATORS + ']' + '.*\\).*', 'gi')) != null
} }
return false return false
} }
@@ -364,7 +369,7 @@ function isAttachmentLink (text) {
function handleAttachmentLinkPaste (storageKey, noteKey, linkText) { function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
if (storageKey != null && noteKey != null && linkText != null) { if (storageKey != null && noteKey != null && linkText != null) {
const storagePath = findStorage.findStorage(storageKey).path const storagePath = findStorage.findStorage(storageKey).path
const attachments = getAttachmentsInContent(linkText) || [] const attachments = getAttachmentsInMarkdownContent(linkText) || []
const replaceInstructions = [] const replaceInstructions = []
const copies = [] const copies = []
for (const attachment of attachments) { for (const attachment of attachments) {
@@ -373,13 +378,13 @@ function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
sander.exists(absPathOfAttachment) sander.exists(absPathOfAttachment)
.then((fileExists) => { .then((fileExists) => {
if (!fileExists) { if (!fileExists) {
const fileNotFoundRegexp = new RegExp('!?' + escapeStringRegexp('[') + '[\\w|\\d|\\s|\\.]*\\]\\(\\s*' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + escapeStringRegexp(path.sep) + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + escapeStringRegexp(')')) const fileNotFoundRegexp = new RegExp('!?' + escapeStringRegexp('[') + '[\\w|\\d|\\s|\\.]*\\]\\(\\s*' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + PATH_SEPARATORS + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + escapeStringRegexp(')'))
replaceInstructions.push({regexp: fileNotFoundRegexp, replacement: this.generateFileNotFoundMarkdown()}) replaceInstructions.push({regexp: fileNotFoundRegexp, replacement: this.generateFileNotFoundMarkdown()})
return Promise.resolve() return Promise.resolve()
} }
return this.copyAttachment(absPathOfAttachment, storageKey, noteKey) return this.copyAttachment(absPathOfAttachment, storageKey, noteKey)
.then((fileName) => { .then((fileName) => {
const replaceLinkRegExp = new RegExp(escapeStringRegexp('(') + ' *' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + escapeStringRegexp(path.sep) + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + ' *' + escapeStringRegexp(')')) const replaceLinkRegExp = new RegExp(escapeStringRegexp('(') + ' *' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + PATH_SEPARATORS + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + ' *' + escapeStringRegexp(')'))
replaceInstructions.push({ replaceInstructions.push({
regexp: replaceLinkRegExp, regexp: replaceLinkRegExp,
replacement: '(' + path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName) + ')' replacement: '(' + path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName) + ')'
@@ -408,7 +413,7 @@ module.exports = {
generateAttachmentMarkdown, generateAttachmentMarkdown,
handleAttachmentDrop, handleAttachmentDrop,
handlePastImageEvent, handlePastImageEvent,
getAttachmentsInContent, getAttachmentsInMarkdownContent,
getAbsolutePathsOfAttachmentsInContent, getAbsolutePathsOfAttachmentsInContent,
removeStorageAndNoteReferences, removeStorageAndNoteReferences,
deleteAttachmentFolder, deleteAttachmentFolder,

View File

@@ -1,5 +1,6 @@
const dataApi = { const dataApi = {
init: require('./init'), init: require('./init'),
toggleStorage: require('./toggleStorage'),
addStorage: require('./addStorage'), addStorage: require('./addStorage'),
renameStorage: require('./renameStorage'), renameStorage: require('./renameStorage'),
removeStorage: require('./removeStorage'), removeStorage: require('./removeStorage'),

View File

@@ -8,7 +8,8 @@ function resolveStorageData (storageCache) {
key: storageCache.key, key: storageCache.key,
name: storageCache.name, name: storageCache.name,
type: storageCache.type, type: storageCache.type,
path: storageCache.path path: storageCache.path,
isOpen: storageCache.isOpen
} }
const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json') const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json')

View File

@@ -0,0 +1,28 @@
const _ = require('lodash')
const resolveStorageData = require('./resolveStorageData')
/**
* @param {String} key
* @param {Boolean} isOpen
* @return {Object} Storage meta data
*/
function toggleStorage (key, isOpen) {
let cachedStorageList
try {
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
} catch (err) {
console.log('error got')
console.error(err)
return Promise.reject(err)
}
const targetStorage = _.find(cachedStorageList, {key: key})
if (targetStorage == null) return Promise.reject('Storage')
targetStorage.isOpen = isOpen
localStorage.setItem('storages', JSON.stringify(cachedStorageList))
return resolveStorageData(targetStorage)
}
module.exports = toggleStorage

View File

@@ -12,8 +12,7 @@ class NewNoteModal extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.state = { this.state = {}
}
} }
componentDidMount () { componentDidMount () {
@@ -35,19 +34,20 @@ class NewNoteModal extends React.Component {
title: '', title: '',
content: '' content: ''
}) })
.then((note) => { .then(note => {
const noteHash = note.key const noteHash = note.key
dispatch({ dispatch({
type: 'UPDATE_NOTE', type: 'UPDATE_NOTE',
note: note note: note
}) })
hashHistory.push({ hashHistory.push({
pathname: location.pathname, pathname: location.pathname,
query: {key: noteHash} query: { key: noteHash }
}) })
ee.emit('list:jump', noteHash) ee.emit('list:jump', noteHash)
ee.emit('detail:focus') ee.emit('detail:focus')
this.props.close() setTimeout(this.props.close, 200)
}) })
} }
@@ -69,13 +69,15 @@ class NewNoteModal extends React.Component {
folder: folder, folder: folder,
title: '', title: '',
description: '', description: '',
snippets: [{ snippets: [
name: '', {
mode: 'text', name: '',
content: '' mode: 'text',
}] content: ''
}
]
}) })
.then((note) => { .then(note => {
const noteHash = note.key const noteHash = note.key
dispatch({ dispatch({
type: 'UPDATE_NOTE', type: 'UPDATE_NOTE',
@@ -83,11 +85,11 @@ class NewNoteModal extends React.Component {
}) })
hashHistory.push({ hashHistory.push({
pathname: location.pathname, pathname: location.pathname,
query: {key: noteHash} query: { key: noteHash }
}) })
ee.emit('list:jump', noteHash) ee.emit('list:jump', noteHash)
ee.emit('detail:focus') ee.emit('detail:focus')
this.props.close() setTimeout(this.props.close, 200)
}) })
} }
@@ -106,49 +108,65 @@ class NewNoteModal extends React.Component {
render () { render () {
return ( return (
<div styleName='root' <div
styleName='root'
tabIndex='-1' tabIndex='-1'
onKeyDown={(e) => this.handleKeyDown(e)} onKeyDown={e => this.handleKeyDown(e)}
> >
<div styleName='header'> <div styleName='header'>
<div styleName='title'>{i18n.__('Make a note')}</div> <div styleName='title'>{i18n.__('Make a note')}</div>
</div> </div>
<ModalEscButton handleEscButtonClick={(e) => this.handleCloseButtonClick(e)} /> <ModalEscButton
handleEscButtonClick={e => this.handleCloseButtonClick(e)}
/>
<div styleName='control'> <div styleName='control'>
<button styleName='control-button' <button
onClick={(e) => this.handleMarkdownNoteButtonClick(e)} styleName='control-button'
onKeyDown={(e) => this.handleMarkdownNoteButtonKeyDown(e)} onClick={e => this.handleMarkdownNoteButtonClick(e)}
onKeyDown={e => this.handleMarkdownNoteButtonKeyDown(e)}
ref='markdownButton' ref='markdownButton'
> >
<i styleName='control-button-icon' <i styleName='control-button-icon' className='fa fa-file-text-o' />
className='fa fa-file-text-o' <br />
/><br /> <span styleName='control-button-label'>
<span styleName='control-button-label'>{i18n.__('Markdown Note')}</span><br /> {i18n.__('Markdown Note')}
<span styleName='control-button-description'>{i18n.__('This format is for creating text documents. Checklists, code blocks and Latex blocks are available.')}</span> </span>
<br />
<span styleName='control-button-description'>
{i18n.__(
'This format is for creating text documents. Checklists, code blocks and Latex blocks are available.'
)}
</span>
</button> </button>
<button styleName='control-button' <button
onClick={(e) => this.handleSnippetNoteButtonClick(e)} styleName='control-button'
onKeyDown={(e) => this.handleSnippetNoteButtonKeyDown(e)} onClick={e => this.handleSnippetNoteButtonClick(e)}
onKeyDown={e => this.handleSnippetNoteButtonKeyDown(e)}
ref='snippetButton' ref='snippetButton'
> >
<i styleName='control-button-icon' <i styleName='control-button-icon' className='fa fa-code' /><br />
className='fa fa-code' <span styleName='control-button-label'>
/><br /> {i18n.__('Snippet Note')}
<span styleName='control-button-label'>{i18n.__('Snippet Note')}</span><br /> </span>
<span styleName='control-button-description'>{i18n.__('This format is for creating code snippets. Multiple snippets can be grouped into a single note.')} <br />
<span styleName='control-button-description'>
{i18n.__(
'This format is for creating code snippets. Multiple snippets can be grouped into a single note.'
)}
</span> </span>
</button> </button>
</div> </div>
<div styleName='description'><i className='fa fa-arrows-h' />{i18n.__('Tab to switch format')}</div> <div styleName='description'>
<i className='fa fa-arrows-h' />{i18n.__('Tab to switch format')}
</div>
</div> </div>
) )
} }
} }
NewNoteModal.propTypes = { NewNoteModal.propTypes = {}
}
export default CSSModules(NewNoteModal, styles) export default CSSModules(NewNoteModal, styles)

View File

@@ -27,7 +27,12 @@ class SnippetEditor extends React.Component {
dragDrop: false, dragDrop: false,
foldGutter: true, foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
autoCloseBrackets: true, autoCloseBrackets: {
pairs: '()[]{}\'\'""$$**``',
triples: '```"""\'\'\'',
explode: '[]{}``$$',
override: true
},
mode: 'null' mode: 'null'
}) })
this.cm.setSize('100%', '100%') this.cm.setSize('100%', '100%')

View File

@@ -4,8 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
const { remote } = require('electron') import context from 'browser/lib/context'
const { Menu, MenuItem } = remote
class SnippetList extends React.Component { class SnippetList extends React.Component {
constructor (props) { constructor (props) {
@@ -21,18 +20,17 @@ class SnippetList extends React.Component {
} }
reloadSnippetList () { reloadSnippetList () {
dataApi.fetchSnippet().then(snippets => this.setState({snippets})) dataApi.fetchSnippet().then(snippets => {
this.setState({snippets})
this.props.onSnippetSelect(this.props.currentSnippet)
})
} }
handleSnippetContextMenu (snippet) { handleSnippetContextMenu (snippet) {
const menu = new Menu() context.popup([{
menu.append(new MenuItem({
label: i18n.__('Delete snippet'), label: i18n.__('Delete snippet'),
click: () => { click: () => this.deleteSnippet(snippet)
this.deleteSnippet(snippet) }])
}
}))
menu.popup()
} }
deleteSnippet (snippet) { deleteSnippet (snippet) {
@@ -43,7 +41,7 @@ class SnippetList extends React.Component {
} }
handleSnippetClick (snippet) { handleSnippetClick (snippet) {
this.props.onSnippetClick(snippet) this.props.onSnippetSelect(snippet)
} }
createSnippet () { createSnippet () {
@@ -55,6 +53,16 @@ class SnippetList extends React.Component {
}).catch(err => { throw err }) }).catch(err => { throw err })
} }
defineSnippetStyleName (snippet) {
const { currentSnippet } = this.props
if (currentSnippet == null) return
if (currentSnippet.id === snippet.id) {
return 'snippet-item-selected'
} else {
return 'snippet-item'
}
}
render () { render () {
const { snippets } = this.state const { snippets } = this.state
return ( return (
@@ -70,7 +78,7 @@ class SnippetList extends React.Component {
{ {
snippets.map((snippet) => ( snippets.map((snippet) => (
<li <li
styleName='snippet-item' styleName={this.defineSnippetStyleName(snippet)}
key={snippet.id} key={snippet.id}
onContextMenu={() => this.handleSnippetContextMenu(snippet)} onContextMenu={() => this.handleSnippetContextMenu(snippet)}
onClick={() => this.handleSnippetClick(snippet)}> onClick={() => this.handleSnippetClick(snippet)}>

View File

@@ -25,7 +25,7 @@ class SnippetTab extends React.Component {
}, 500) }, 500)
} }
handleSnippetClick (snippet) { handleSnippetSelect (snippet) {
const { currentSnippet } = this.state const { currentSnippet } = this.state
if (currentSnippet === null || currentSnippet.id !== snippet.id) { if (currentSnippet === null || currentSnippet.id !== snippet.id) {
dataApi.fetchSnippet(snippet.id).then(changedSnippet => { dataApi.fetchSnippet(snippet.id).then(changedSnippet => {
@@ -66,8 +66,9 @@ class SnippetTab extends React.Component {
<div styleName='root'> <div styleName='root'>
<div styleName='header'>{i18n.__('Snippets')}</div> <div styleName='header'>{i18n.__('Snippets')}</div>
<SnippetList <SnippetList
onSnippetClick={this.handleSnippetClick.bind(this)} onSnippetSelect={this.handleSnippetSelect.bind(this)}
onSnippetDeleted={this.handleDeleteSnippet.bind(this)} /> onSnippetDeleted={this.handleDeleteSnippet.bind(this)}
currentSnippet={currentSnippet} />
<div styleName='snippet-detail' style={{visibility: currentSnippet ? 'visible' : 'hidden'}}> <div styleName='snippet-detail' style={{visibility: currentSnippet ? 'visible' : 'hidden'}}>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Snippet name')}</div> <div styleName='group-section-label'>{i18n.__('Snippet name')}</div>

View File

@@ -122,6 +122,10 @@
&:hover &:hover
background darken(#f5f5f5, 5) background darken(#f5f5f5, 5)
.snippet-item-selected
@extend .snippet-list .snippet-item
background darken(#f5f5f5, 5)
.snippet-detail .snippet-detail
width 70% width 70%
height calc(100% - 200px) height calc(100% - 200px)
@@ -142,6 +146,8 @@ body[data-theme="default"], body[data-theme="white"]
background $ui-borderColor background $ui-borderColor
&:hover &:hover
background darken($ui-backgroundColor, 5) background darken($ui-backgroundColor, 5)
.snippet-item-selected
background darken($ui-backgroundColor, 5)
body[data-theme="dark"] body[data-theme="dark"]
.snippets .snippets
@@ -152,8 +158,12 @@ body[data-theme="dark"]
background $ui-dark-borderColor background $ui-dark-borderColor
&:hover &:hover
background darken($ui-dark-backgroundColor, 5) background darken($ui-dark-backgroundColor, 5)
.snippet-item-selected
background darken($ui-dark-backgroundColor, 5)
.snippet-detail .snippet-detail
color white color white
.group-control-button
colorDarkPrimaryButton()
body[data-theme="solarized-dark"] body[data-theme="solarized-dark"]
.snippets .snippets
@@ -164,8 +174,12 @@ body[data-theme="solarized-dark"]
background $ui-solarized-dark-borderColor background $ui-solarized-dark-borderColor
&:hover &:hover
background darken($ui-solarized-dark-backgroundColor, 5) background darken($ui-solarized-dark-backgroundColor, 5)
.snippet-item-selected
background darken($ui-solarized-dark-backgroundColor, 5)
.snippet-detail .snippet-detail
color white color white
.group-control-button
colorSolarizedDarkPrimaryButton()
body[data-theme="monokai"] body[data-theme="monokai"]
.snippets .snippets
@@ -176,5 +190,9 @@ body[data-theme="monokai"]
background $ui-monokai-borderColor background $ui-monokai-borderColor
&:hover &:hover
background darken($ui-monokai-backgroundColor, 5) background darken($ui-monokai-backgroundColor, 5)
.snippet-item-selected
background darken($ui-monokai-backgroundColor, 5)
.snippet-detail .snippet-detail
color white color white
.group-control-button
colorMonokaiPrimaryButton()

View File

@@ -11,6 +11,7 @@ import 'codemirror-mode-elixir'
import _ from 'lodash' import _ from 'lodash'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import { getLanguages } from 'browser/lib/Languages' import { getLanguages } from 'browser/lib/Languages'
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
@@ -164,7 +165,7 @@ class UiTab extends React.Component {
const { config, codemirrorTheme } = this.state const { config, codemirrorTheme } = this.state
const codemirrorSampleCode = 'function iamHappy (happy) {\n\tif (happy) {\n\t console.log("I am Happy!")\n\t} else {\n\t console.log("I am not Happy!")\n\t}\n};' const codemirrorSampleCode = 'function iamHappy (happy) {\n\tif (happy) {\n\t console.log("I am Happy!")\n\t} else {\n\t console.log("I am not Happy!")\n\t}\n};'
const enableEditRulersStyle = config.editor.enableRulers ? 'block' : 'none' const enableEditRulersStyle = config.editor.enableRulers ? 'block' : 'none'
const customCSS = config.preview.customCSS const fontFamily = normalizeEditorFontFamily(config.editor.fontFamily)
return ( return (
<div styleName='root'> <div styleName='root'>
<div styleName='group'> <div styleName='group'>
@@ -262,8 +263,16 @@ class UiTab extends React.Component {
}) })
} }
</select> </select>
<div styleName='code-mirror'> <div styleName='code-mirror' style={{fontFamily}}>
<ReactCodeMirror ref={e => (this.codeMirrorInstance = e)} value={codemirrorSampleCode} options={{ lineNumbers: true, readOnly: true, mode: 'javascript', theme: codemirrorTheme }} /> <ReactCodeMirror
ref={e => (this.codeMirrorInstance = e)}
value={codemirrorSampleCode}
options={{
lineNumbers: true,
readOnly: true,
mode: 'javascript',
theme: codemirrorTheme
}} />
</div> </div>
</div> </div>
</div> </div>
@@ -596,7 +605,19 @@ class UiTab extends React.Component {
type='checkbox' type='checkbox'
/>&nbsp; />&nbsp;
{i18n.__('Allow custom CSS for preview')} {i18n.__('Allow custom CSS for preview')}
<ReactCodeMirror onChange={e => this.handleUIChange(e)} ref={e => (this.customCSSCM = e)} value={config.preview.customCSS} options={{ lineNumbers: true, mode: 'css', theme: codemirrorTheme }} /> <div style={{fontFamily}}>
<ReactCodeMirror
width='400px'
height='400px'
onChange={e => this.handleUIChange(e)}
ref={e => (this.customCSSCM = e)}
value={config.preview.customCSS}
options={{
lineNumbers: true,
mode: 'css',
theme: codemirrorTheme
}} />
</div>
</div> </div>
</div> </div>

View File

@@ -360,6 +360,12 @@ function data (state = defaultDataMap(), action) {
state.storageMap = new Map(state.storageMap) state.storageMap = new Map(state.storageMap)
state.storageMap.set(action.storage.key, action.storage) state.storageMap.set(action.storage.key, action.storage)
return state return state
case 'EXPAND_STORAGE':
state = Object.assign({}, state)
state.storageMap = new Map(state.storageMap)
action.storage.isOpen = action.isOpen
state.storageMap.set(action.storage.key, action.storage)
return state
} }
return state return state
} }

76
dev-scripts/dev.js Normal file
View File

@@ -0,0 +1,76 @@
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
const config = require('../webpack.config')
const signale = require('signale')
const { spawn } = require('child_process')
const electron = require('electron')
const port = 8080
let server = null
let firstRun = true
const options = {
publicPath: config.output.publicPath,
hot: true,
inline: true,
quiet: true
}
function startServer () {
config.plugins.push(new webpack.HotModuleReplacementPlugin())
config.entry.main.unshift(
`webpack-dev-server/client?http://localhost:${port}/`,
'webpack/hot/dev-server'
)
const compiler = webpack(config)
server = new WebpackDevServer(compiler, options)
return new Promise((resolve, reject) => {
server.listen(port, 'localhost', function (err) {
if (err) {
reject(err)
}
signale.success(`Webpack Dev Server listening at localhost:${port}`)
signale.watch(`Waiting for webpack to bundle...`)
compiler.plugin('done', stats => {
if (!stats.hasErrors()) {
signale.success(`Bundle success !`)
resolve()
} else {
if (!firstRun) {
console.log(stats.compilation.errors[0])
} else {
firstRun = false
reject(stats.compilation.errors[0])
}
}
})
})
})
}
function startElectron () {
spawn(electron, ['--hot', './index.js'])
.on('close', () => {
server.close()
})
.on('error', err => {
signale.error(err)
server.close()
})
.on('disconnect', () => {
server.close()
})
.on('exit', () => {
server.close()
})
}
startServer()
.then(() => {
startElectron()
signale.success('Electron started')
})
.catch(err => {
signale.error(err)
process.exit(1)
})

View File

@@ -2,10 +2,9 @@
This page is also available in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) and [German](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md). This page is also available in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) and [German](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md).
## Environments ## Environments
* npm: 4.x
* node: 7.x
You should use `npm v4.x` because `$ grunt pre-build` fails on `v5.x`. * npm: 6.x
* node: 8.x
## Development ## Development
@@ -21,17 +20,9 @@ $ yarn
Build and run. Build and run.
``` ```
$ yarn run dev-start $ yarn run dev
``` ```
This command runs `yarn run webpack` and `yarn run hot` in parallel. It is the same as running these commands in two terminals.
The `webpack` will watch for code changes and then apply them automatically.
If the following error occurs: `Failed to load resource: net::ERR_CONNECTION_REFUSED`, please reload Boostnote.
![net::ERR_CONNECTION_REFUSED](https://cloud.githubusercontent.com/assets/11307908/24343004/081e66ae-1279-11e7-8d9e-7f478043d835.png)
> ### Notice > ### Notice
> There are some cases where you have to refresh the app manually. > There are some cases where you have to refresh the app manually.
> 1. When editing a constructor method of a component > 1. When editing a constructor method of a component
@@ -44,8 +35,6 @@ You can build the program by using `grunt`. However, we don't recommend this bec
So, we've prepared a separate script which just makes an executable file. So, we've prepared a separate script which just makes an executable file.
This build doesn't work on npm v5.3.0. So you need to use v5.2.0 when you build it.
``` ```
grunt pre-build grunt pre-build
``` ```

View File

@@ -2,10 +2,9 @@
Diese Seite ist auch verfügbar in [Japanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Koreanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Vereinfachtem Chinesisch](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [Französisch](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) und [Deutsch](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md). Diese Seite ist auch verfügbar in [Japanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Koreanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Vereinfachtem Chinesisch](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [Französisch](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) und [Deutsch](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md).
## Umgebungen ## Umgebungen
* npm: 4.x
* node: 7.x
Du solltest `npm v4.x` benutzen weil `$ grunt pre-build` scheitert mit Version `v5.x`. * npm: 6.x
* node: 8.x
## Entwicklung ## Entwicklung
@@ -21,17 +20,9 @@ $ yarn
Bauen und Ausführen. Bauen und Ausführen.
``` ```
$ yarn run dev-start $ yarn run dev
``` ```
Dieser Befehl startet `yarn run webpack` und `yarn run hot` parallel. Es hat den selben Effekt wie beide Befehle separat in zwei Terminals zu starten.
Das `webpack` überprüft den Code auf Änderungen und wendet diese dann automatisch an.
Wenn folgender Fehler passiert: `Failed to load resource: net::ERR_CONNECTION_REFUSED`, bitte Boostnote neu starten.
![net::ERR_CONNECTION_REFUSED](https://cloud.githubusercontent.com/assets/11307908/24343004/081e66ae-1279-11e7-8d9e-7f478043d835.png)
> ### Notiz > ### Notiz
> Es gibt einige Fälle bei denen die App manuell zu refreshen ist. > Es gibt einige Fälle bei denen die App manuell zu refreshen ist.
> 1. Wenn eine "constructor method" einer Komponente manuell editiert wird. > 1. Wenn eine "constructor method" einer Komponente manuell editiert wird.
@@ -44,11 +35,10 @@ Du kannst das Programm unter Verwendung von `grunt` bauen. Jedoch empfehlen wir
Deshalb haben wir ein separates Script vorbereitet welches eine ausführbare Datei erstellt. Deshalb haben wir ein separates Script vorbereitet welches eine ausführbare Datei erstellt.
Dieser build funktioniert nicht mit npm v5.3.0. Deshalb musst du für den Build die Version v5.2.0 verwenden.
``` ```
grunt pre-build grunt pre-build
``` ```
Du findest die ausführbare Datein in dem Verzeichnis `dist`. Beachte, der auto updater funktioniert nicht da die app nicht signiert ist. Du findest die ausführbare Datein in dem Verzeichnis `dist`. Beachte, der auto updater funktioniert nicht da die app nicht signiert ist.
Wenn du es für notwendig erachtest, kannst du codesign or authenticode mit dieser ausführbaren Datei verwenden. Wenn du es für notwendig erachtest, kannst du codesign or authenticode mit dieser ausführbaren Datei verwenden.

View File

@@ -2,10 +2,9 @@
Cette page est également disponible en [Anglais](https://github.com/BoostIO/Boostnote/blob/master/docs/build.md), [Japonais](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Coréen](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russe](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Chinois Simplifié](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md) et en [Allemand](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md) Cette page est également disponible en [Anglais](https://github.com/BoostIO/Boostnote/blob/master/docs/build.md), [Japonais](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Coréen](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russe](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Chinois Simplifié](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md) et en [Allemand](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md)
## Environnements ## Environnements
* npm: 4.x
* node: 7.x
Il est conseillé d'utiliser `npm v4.x` car `$ grunt pre-build` ne marche pas sur la `v5.x`. * npm: 6.x
* node: 8.x
## Développement ## Développement
@@ -20,17 +19,9 @@ $ yarn
Build et start Build et start
``` ```
$ yarn run dev-start $ yarn run dev
``` ```
Cette commande lance `yarn run webpack` et `yarn run hot` en parallèle. Cela revient au même que si on utilisait ces deux commandes dans 2 terminaux.
La commande `webpack` va surveiller les changements de code et les appliquer automatiquement.
Si l'erreur suivante apparait : `Failed to load resource: net::ERR_CONNECTION_REFUSED`, relancez Boostnote.
![net::ERR_CONNECTION_REFUSED](https://cloud.githubusercontent.com/assets/11307908/24343004/081e66ae-1279-11e7-8d9e-7f478043d835.png)
> ### Notice > ### Notice
> Il y a certains cas où vous voudrez relancer l'application manuellement. > Il y a certains cas où vous voudrez relancer l'application manuellement.
> 1. Quand vous éditez la méthode constructeur dans un composant > 1. Quand vous éditez la méthode constructeur dans un composant
@@ -43,8 +34,6 @@ Vous pouvez build le programme en utilisant `grunt`. Cependant, nous ne recomman
Nous avons donc préparé un script séparé qui va rendre un fichier exécutable. Nous avons donc préparé un script séparé qui va rendre un fichier exécutable.
Le build ne fonctionne pas sur `npm v5.3.0`. Il faut donc utiliser `npm v5.2.0` quand vous faites le build.
``` ```
grunt pre-build grunt pre-build
``` ```

View File

@@ -1,9 +1,15 @@
# Build # Build
## 環境
* npm: 6.x
* node: 8.x
## 開発 ## 開発
Webpack HRMを使います。 Webpack HRMを使います。
次の命令から私達がしておいた設定を使うことができます。 Boostnoteの最上位ディレクトリにて以下のコマンドを実行して、
デフォルトの設定の開発環境を起動させます。
依存するパッケージをインストールします。 依存するパッケージをインストールします。
@@ -14,30 +20,20 @@ $ yarn
ビルドして実行します。 ビルドして実行します。
``` ```
$ yarn run dev-start $ yarn run dev
``` ```
このコマンドは `yarn run webpack``yarn run hot`を並列に実行します。つまりこのコマンドは2つのターミナルで同時にこれらのコマンドを実行するのと同じことです。
そして、Webpackが自動的にコードの変更を確認し、それを適用してくれるようになります。
もし、 `Failed to load resource: net::ERR_CONNECTION_REFUSED`というエラーが起きた場合、Boostnoteをリロードしてください。
![net::ERR_CONNECTION_REFUSED](https://cloud.githubusercontent.com/assets/11307908/24343004/081e66ae-1279-11e7-8d9e-7f478043d835.png)
> ### 注意 > ### 注意
> 時々、直接リフレッシュをする必要があります。 > 時々、直接リフレッシュをする必要があります。
> 1. コンポネントのコンストラクタ関数を編集する場合 > 1. コンポネントのコンストラクタ関数を編集する場合
> 2. 新しいCSSクラスを追加する場合(1.の理由と同じ: CSSクラス名はコンポネントごとに書きなおされまが、この作業はコンストラクタで行われます。) > 2. 新しいCSSクラスを追加する場合(1.の理由と同じ: CSSクラス名はコンポネントごとに書きなおされまが、この作業はコンストラクタで行われます。)
## 配布 ## 配布
Gruntを使います。 Gruntを使います。
実際の配布は`grunt`で実行できます。しかし、これにはCodesignとAuthenticodeの仮定が含まれるので、使っては行けないです 実際の配布は`grunt`で実行できます。しかし、これにはCodesignとAuthenticodeを実行するタスクが含まれるので、使用しないでください
それで、実行ファイルを作るスクリプトを用意しておきました。 代わりに、実行ファイルを作るスクリプトを用意しておきました。
このビルドはnpm v5.3.0では動かないのでv5.2.0で動かす必要があります。
``` ```
grunt pre-build grunt pre-build

View File

@@ -1,8 +1,9 @@
# Build # Build
## 환경 ## 환경
* npm: 4.x
* node: 7.x * npm: 6.x
* node: 8.x
`$ grunt pre-build``npm v5.x`에서 실행할 수 없기 때문에, 반드시 `npm v4.x`를 사용하셔야 합니다. `$ grunt pre-build``npm v5.x`에서 실행할 수 없기 때문에, 반드시 `npm v4.x`를 사용하셔야 합니다.
@@ -20,17 +21,9 @@ $ yarn
그 다음, 아래의 명령으로 빌드를 끝내고 자동적으로 어플리케이션을 실행합니다. 그 다음, 아래의 명령으로 빌드를 끝내고 자동적으로 어플리케이션을 실행합니다.
``` ```
$ yarn run dev-start $ yarn run dev
``` ```
이 명령은 `yarn run webpack``yarn run hot`을 동시에 실행합니다. 이는 두개의 터미널에서 각각의 명령을 동시에 실행하는 것과 같습니다.
`Webpack`은 코드의 변화를 자동으로 탐지하여 적용시키는 역할을 합니다.
만약, `Failed to load resource: net::ERR_CONNECTION_REFUSED`과 같은 에러가 나타난다면 Boostnote를 리로드해주세요.
![net::ERR_CONNECTION_REFUSED](https://cloud.githubusercontent.com/assets/11307908/24343004/081e66ae-1279-11e7-8d9e-7f478043d835.png)
> ### 주의 > ### 주의
> 가끔 직접 리프레쉬를 해주어야 하는 경우가 있습니다. > 가끔 직접 리프레쉬를 해주어야 하는 경우가 있습니다.
> 1. 콤포넌트의 컨스트럭터 함수를 수정할 경우 > 1. 콤포넌트의 컨스트럭터 함수를 수정할 경우
@@ -43,8 +36,6 @@ Boostnote에서는 배포 자동화를 위하여 그런트를 사용합니다.
그래서, 실행파일만을 만드는 스크립트를 준비해 뒀습니다. 그래서, 실행파일만을 만드는 스크립트를 준비해 뒀습니다.
이 빌드는 npm v5.3.0에서는 작동하지 않습니다. 그러므로, 성공적으로 빌드하기 위해서는 v5.2.0을 사용해야 합니다.
``` ```
grunt pre-build grunt pre-build
``` ```

View File

@@ -1,10 +1,9 @@
# Сборка # Сборка
## Используемые инструменты ## Используемые инструменты
* npm: 4.x
* node: 7.x
Вы должны использовать `npm v4.x`, так как `$ grunt pre-build` не работает в `v5.x`. * npm: 6.x
* node: 8.x
## Разработка ## Разработка
@@ -20,17 +19,9 @@ $ yarn
Соберите и запустите. Соберите и запустите.
``` ```
$ yarn run dev-start $ yarn run dev
``` ```
Эта команда выполняет `yarn run webpack` и `yarn run hot` параллельно. Результат будет такой же, если вы выполните эти две команды раздельно.
`Webpack` будет следить за изменениями в коде и будет применять их автоматически.
Если возникает следующая ошибка: `Failed to load resource: net::ERR_CONNECTION_REFUSED`, пожалуйста, перезапустите Boostnote.
![net::ERR_CONNECTION_REFUSED](https://cloud.githubusercontent.com/assets/11307908/24343004/081e66ae-1279-11e7-8d9e-7f478043d835.png)
> ### Примечание > ### Примечание
> В некоторых случаях вам необходимо обновить приложение вручную. > В некоторых случаях вам необходимо обновить приложение вручную.
> 1. При редактировании метода конструктора компонента > 1. При редактировании метода конструктора компонента
@@ -41,9 +32,7 @@ $ yarn run dev-start
Мы используем Grunt для автоматического деплоя. Мы используем Grunt для автоматического деплоя.
Вы можете создать задачу, используя `grunt`. Однако мы не рекомендуем этого делать, так как задача по умолчанию включает в себя код и аутентификацию. Вы можете создать задачу, используя `grunt`. Однако мы не рекомендуем этого делать, так как задача по умолчанию включает в себя код и аутентификацию.
Мы подготовили отдельный скрипт, который просто создает исполняемый файл: Мы подготовили отдельный скрипт, который просто создает исполняемый файл.
This build doesn't work on npm v5.3.0. So you need to use v5.2.0 when you build it.
``` ```
grunt pre-build grunt pre-build

View File

@@ -1,37 +1,27 @@
# 构建Boostnote # 构建Boostnote
## 环境 ## 环境
* npm: 4.x
* node: 7.x
因为`$ grunt pre-build`的问题,您只能使用`npm v4.x`而不能使用`npm v5.x` * npm: 6.x
* node: 8.x
## 开发 ## 开发
我们使用Webpack HMR来开发Boostnote。 我们使用Webpack HMR来开发Boostnote。
在代码根目录下运行下列指令可以以默认配置运行Boostnote。 在代码根目录下运行下列指令可以以默认配置运行Boostnote。
### 首先使用yarn安装所需的依赖包。 ### 首先使用yarn安装所需的依赖包。
``` ```
$ yarn $ yarn
``` ```
### 接着编译并且运行Boostnote。 ### 接着编译并且运行Boostnote。
``` ```
$ yarn run dev-start $ yarn run dev
``` ```
这个指令相当于在两个终端内同时运行`yarn run webpack``yarn run hot`
如果出现错误`Failed to load resource: net::ERR_CONNECTION_REFUSED`请尝试重新运行Boostnote。
![net::ERR_CONNECTION_REFUSED](https://cloud.githubusercontent.com/assets/11307908/24343004/081e66ae-1279-11e7-8d9e-7f478043d835.png)
### 然后您就可以进行开发了
当您对代码作出更改的时候,`webpack`会自动抓取并应用所有代码更改。
> ### 提示 > ### 提示
> 在如下情况中您可能需要重新运行Boostnote才能应用代码更改 > 在如下情况中您可能需要重新运行Boostnote才能应用代码更改
> 1. 当您在修改了一个组件的构造函数的时候When editing a constructor method of a component > 1. 当您在修改了一个组件的构造函数的时候When editing a constructor method of a component
@@ -39,18 +29,16 @@ $ yarn run dev-start
## 部署 ## 部署
我们使用Grunt来自动部署Boostnote。 我们使用Grunt来自动部署Boostnote。
因为部署需要协同设计(codesign)与验证码(authenticode),所以您可以但我们不建议通过`grunt`来部署。 因为部署需要协同设计(codesign)与验证码(authenticode),所以您可以但我们不建议通过`grunt`来部署。
所以我们准备了一个脚本文件来生成执行文件。 所以我们准备了一个脚本文件来生成执行文件。
``` ```
grunt pre-build grunt pre-build
``` ```
您只能使用`npm v5.2.0`而不能使用`npm v5.3.0` 接下来您就可以在`dist`目录中找到可执行文件。
接下来您就可以在`dist`目录中找到可执行文件。
> ### 提示 > ### 提示
> 因为此可执行文件并没有被注册,所以自动更新不可用。 > 因为此可执行文件并没有被注册,所以自动更新不可用。
> 如果需要,您也可将协同设计(codesign)与验证码(authenticode)使用于这个可执行文件中。 > 如果需要,您也可将协同设计(codesign)与验证码(authenticode)使用于这个可执行文件中。

View File

@@ -2,10 +2,9 @@
此文件還提供下列的語言 [日文](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [韓文](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [俄文](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [簡體中文](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [法文](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) and [德文](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md). 此文件還提供下列的語言 [日文](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [韓文](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [俄文](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [簡體中文](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [法文](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) and [德文](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md).
## 環境 ## 環境
* npm: 4.x
* node: 7.x
`$ grunt pre-build``npm v5.x` 有問題,所以只能用 `npm v4.x` * npm: 6.x
* node: 8.x
## 開發 ## 開發
@@ -22,18 +21,9 @@ $ yarn
**開始開發** **開始開發**
``` ```
$ yarn run dev-start $ yarn run dev
``` ```
上述指令同時運行了 `yarn run webpack``yarn run hot`,相當於將這兩個指令在不同的 terminal 中運行。
`webpack` 會同時監控修改過的程式碼,並
The `webpack` will watch for code changes and then apply them automatically.
If the following error occurs: `Failed to load resource: net::ERR_CONNECTION_REFUSED`, please reload Boostnote.
![net::ERR_CONNECTION_REFUSED](https://cloud.githubusercontent.com/assets/11307908/24343004/081e66ae-1279-11e7-8d9e-7f478043d835.png)
> ### Notice > ### Notice
> There are some cases where you have to refresh the app manually. > There are some cases where you have to refresh the app manually.
> 1. When editing a constructor method of a component > 1. When editing a constructor method of a component
@@ -46,8 +36,6 @@ You can build the program by using `grunt`. However, we don't recommend this bec
So, we've prepared a separate script which just makes an executable file. So, we've prepared a separate script which just makes an executable file.
This build doesn't work on npm v5.3.0. So you need to use v5.2.0 when you build it.
``` ```
grunt pre-build grunt pre-build
``` ```

View File

@@ -90,7 +90,7 @@ app.on('ready', function () {
mainWindow.setMenu(menu) mainWindow.setMenu(menu)
} }
// Check update every hour // Check update every day
setInterval(function () { setInterval(function () {
checkUpdate() checkUpdate()
}, 1000 * 60 * 60 * 24) }, 1000 * 60 * 60 * 24)
@@ -106,7 +106,7 @@ app.on('ready', function () {
checkUpdate() checkUpdate()
} }
}) })
}, 10000) }, 10 * 1000)
ipcServer = require('./ipcServer') ipcServer = require('./ipcServer')
ipcServer.server.start() ipcServer.server.start()
}) })

View File

@@ -136,6 +136,15 @@ const file = {
{ {
type: 'separator' type: 'separator'
}, },
{
label: 'Format Table',
click () {
mainWindow.webContents.send('code:format-table')
}
},
{
type: 'separator'
},
{ {
label: 'Print', label: 'Print',
accelerator: 'CommandOrControl+P', accelerator: 'CommandOrControl+P',
@@ -209,6 +218,16 @@ const edit = {
label: 'Select All', label: 'Select All',
accelerator: 'Command+A', accelerator: 'Command+A',
selector: 'selectAll:' selector: 'selectAll:'
},
{
type: 'separator'
},
{
label: 'Add Tag',
accelerator: 'CommandOrControl+Shift+T',
click () {
mainWindow.webContents.send('editor:add-tag')
}
} }
] ]
} }
@@ -235,14 +254,14 @@ const view = {
}, },
{ {
label: 'Next Note', label: 'Next Note',
accelerator: 'Control+J', accelerator: 'CommandOrControl+]',
click () { click () {
mainWindow.webContents.send('list:next') mainWindow.webContents.send('list:next')
} }
}, },
{ {
label: 'Previous Note', label: 'Previous Note',
accelerator: 'Control+K', accelerator: 'CommandOrControl+[',
click () { click () {
mainWindow.webContents.send('list:prior') mainWindow.webContents.send('list:prior')
} }
@@ -267,6 +286,19 @@ const view = {
mainWindow.setFullScreen(!mainWindow.isFullScreen()) mainWindow.setFullScreen(!mainWindow.isFullScreen())
} }
}, },
{
type: 'separator'
},
{
label: 'Toggle Side Bar',
accelerator: 'CommandOrControl+B',
click () {
mainWindow.webContents.send('editor:fullscreen')
}
},
{
type: 'separator'
},
{ {
role: 'zoomin', role: 'zoomin',
accelerator: macOS ? 'CommandOrControl+Plus' : 'Control+=' accelerator: macOS ? 'CommandOrControl+Plus' : 'Control+='

View File

@@ -7,9 +7,11 @@ const config = new Config()
const _ = require('lodash') const _ = require('lodash')
var showMenu = process.platform !== 'win32' var showMenu = process.platform !== 'win32'
const windowSize = config.get('windowsize') || { width: 1080, height: 720 } const windowSize = config.get('windowsize') || { x: null, y: null, width: 1080, height: 720 }
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
x: windowSize.x,
y: windowSize.y,
width: windowSize.width, width: windowSize.width,
height: windowSize.height, height: windowSize.height,
minWidth: 500, minWidth: 500,
@@ -59,6 +61,7 @@ if (process.platform === 'darwin') {
} }
mainWindow.on('resize', _.throttle(storeWindowSize, 500)) mainWindow.on('resize', _.throttle(storeWindowSize, 500))
mainWindow.on('move', _.throttle(storeWindowSize, 500))
function storeWindowSize () { function storeWindowSize () {
try { try {

View File

@@ -1,72 +1,83 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<link rel="stylesheet" href="../node_modules/font-awesome/css/font-awesome.min.css" media="screen" charset="utf-8"> <link rel="stylesheet" href="../node_modules/font-awesome/css/font-awesome.min.css" media="screen" charset="utf-8">
<link rel="shortcut icon" href="../resources/favicon.ico"> <link rel="shortcut icon" href="../resources/favicon.ico">
<link rel="stylesheet" href="../node_modules/codemirror/lib/codemirror.css"> <link rel="stylesheet" href="../node_modules/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="../node_modules/katex/dist/katex.min.css">
<link rel="stylesheet" href="../node_modules/codemirror/addon/dialog/dialog.css"> <link rel="stylesheet" href="../node_modules/codemirror/addon/dialog/dialog.css">
<title>Boostnote</title> <title>Boostnote</title>
<style> <style>
@font-face { @font-face {
font-family: 'OpenSans'; font-family: 'OpenSans';
src: url('../resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */ src: url('../resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('../resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */ url('../resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('../resources/fonts/Lato-Regular.ttf') format('truetype'); url('../resources/fonts/Lato-Regular.ttf') format('truetype');
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
@font-face {
font-family: 'Lato'; @font-face {
src: url('../resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */ font-family: 'Lato';
url('../resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */ src: url('../resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('../resources/fonts/Lato-Regular.ttf') format('truetype'); url('../resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
font-style: normal; url('../resources/fonts/Lato-Regular.ttf') format('truetype');
font-weight: normal; font-style: normal;
text-rendering: optimizeLegibility; font-weight: normal;
} text-rendering: optimizeLegibility;
#loadingCover{ }
background-color: #f4f4f4;
position: absolute; #loadingCover {
top: 0; background-color: #f4f4f4;
bottom: 0; position: absolute;
left: 0; top: 0;
right: 0; bottom: 0;
box-sizing: border-box; left: 0;
padding: 65px 0; right: 0;
font-family: sans-serif; box-sizing: border-box;
} padding: 65px 0;
#loadingCover img{ font-family: sans-serif;
display: block; }
margin: 75px auto 5px;
width: 160px; #loadingCover img {
height: 160px; display: block;
} margin: 75px auto 5px;
#loadingCover .message{ width: 160px;
font-size: 30px; height: 160px;
text-align: center; }
line-height: 1.6;
font-weight: 100; #loadingCover .message {
color: #888; font-size: 30px;
} text-align: center;
.CodeEditor { line-height: 1.6;
opacity: 1 !important; font-weight: 100;
pointer-events: auto !important; color: #888;
} }
.CodeMirror-ruler {
border-left-color: rgba(142, 142, 142, 0.5); .CodeEditor {
mix-blend-mode: difference; opacity: 1 !important;
} pointer-events: auto !important;
}
.CodeMirror-ruler {
border-left-color: rgba(142, 142, 142, 0.5);
mix-blend-mode: difference;
}
</style> </style>
</head> </head>
<body> <body>
<div id="loadingCover"> <div id="loadingCover">
<img src="../resources/app.png"> <img src="../resources/app.png">
<div class='message'><i class="fa fa-spinner fa-spin" spin></i></div> <div class='message'>
<i class="fa fa-spinner fa-spin" spin></i>
</div>
</div> </div>
<div id="content"></div> <div id="content"></div>
@@ -130,4 +141,5 @@
} }
</style> </style>
</body> </body>
</html>
</html>

View File

@@ -141,16 +141,16 @@
"Portuguese": "Portugais", "Portuguese": "Portugais",
"Spanish": "Espagnol", "Spanish": "Espagnol",
"You have to save!": "Il faut sauvegarder !", "You have to save!": "Il faut sauvegarder !",
"Russian": "Russian", "Russian": "Russe",
"Command(⌘)": "Command(⌘)", "Command(⌘)": "Command(⌘)",
"Editor Rulers": "Editor Rulers", "Editor Rulers": "Règles dans l'éditeur",
"Enable": "Enable", "Enable": "Activer",
"Disable": "Disable", "Disable": "Désactiver",
"Allow preview to scroll past the last line": "Allow preview to scroll past the last line", "Allow preview to scroll past the last line": "Permettre de scroller après la dernière ligne dans l'aperçu",
"Sanitization": "Sanitization", "Sanitization": "Sanitization",
"Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)", "Only allow secure html tags (recommended)": "N'accepter que les tags html sécurisés (recommandé)",
"Allow styles": "Allow styles", "Allow styles": "Accepter les styles",
"Allow dangerous html tags": "Allow dangerous html tags", "Allow dangerous html tags": "Accepter les tags html dangereux",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.", "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convertir des flèches textuelles en jolis signes. ⚠ Cela va interferérer avec les éventuels commentaires HTML dans votre Markdown.",
"⚠ 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! ⚠" "⚠ 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! ⚠": "⚠ Vous avez collé un lien qui référence une pièce-jointe qui n'a pas pu être récupéré dans le dossier de stockage de la note. Coller des liens qui font référence à des pièces-jointes ne fonctionne que si la source et la destination et la même. Veuillez plutôt utiliser du Drag & Drop ! ⚠"
} }

View File

@@ -4,10 +4,10 @@
"Preferences": "偏好設定", "Preferences": "偏好設定",
"Make a note": "做點筆記", "Make a note": "做點筆記",
"Ctrl": "Ctrl", "Ctrl": "Ctrl",
"Ctrl(^)": "Ctrl", "Ctrl(^)": "Ctrl(^)",
"to create a new note": "新增筆記", "to create a new note": "新增筆記",
"Toggle Mode": "切換模式", "Toggle Mode": "切換模式",
"Trash": "廢紙簍", "Trash": "垃圾桶",
"MODIFICATION DATE": "修改時間", "MODIFICATION DATE": "修改時間",
"Words": "單字", "Words": "單字",
"Letters": "字數", "Letters": "字數",
@@ -20,8 +20,8 @@
".html": ".html", ".html": ".html",
"Print": "列印", "Print": "列印",
"Your preferences for Boostnote": "Boostnote 偏好設定", "Your preferences for Boostnote": "Boostnote 偏好設定",
"Storages": "本機儲存空間", "Storages": "儲存空間",
"Add Storage Location": "新增一個本機儲存位置", "Add Storage Location": "新增儲存位置",
"Add Folder": "新增資料夾", "Add Folder": "新增資料夾",
"Open Storage folder": "開啟儲存資料夾", "Open Storage folder": "開啟儲存資料夾",
"Unlink": "解除連結", "Unlink": "解除連結",
@@ -43,12 +43,12 @@
"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": "在編輯器貼上網址的時候,自動加上網頁標題", "Bring in web page title when pasting URL on editor": "在編輯器貼上網址的時候,自動加上網頁標題",
@@ -79,7 +79,7 @@
"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 everyone,": "親愛的用戶:", "Dear everyone,": "親愛的用戶:",
@@ -104,9 +104,9 @@
"Confirm": "確認", "Confirm": "確認",
"Cancel": "取消", "Cancel": "取消",
"Markdown Note": "Markdown 筆記", "Markdown Note": "Markdown 筆記",
"This format is for creating text documents. Checklists, code blocks and Latex blocks are available.": "建立文件、清單,也可以使用程式碼區塊甚至是 Latex 區塊。", "This format is for creating text documents. Checklists, code blocks and Latex blocks are available.": "建立文件、清單,也可以使用程式碼區塊 Latex 區塊。",
"Snippet Note": "程式碼片段筆記", "Snippet Note": "程式碼片段筆記",
"This format is for creating code snippets. Multiple snippets can be grouped into a single note.": "建立程式碼區塊片段。個程式碼區塊可以合在同一個筆記。", "This format is for creating code snippets. Multiple snippets can be grouped into a single note.": "建立程式碼區塊片段。個程式碼區塊可以分組爲同一個筆記。",
"Tab to switch format": "使用 Tab 鍵切換格式", "Tab to switch format": "使用 Tab 鍵切換格式",
"Updated": "依更新時間排序", "Updated": "依更新時間排序",
"Created": "依建立時間排序", "Created": "依建立時間排序",
@@ -117,12 +117,12 @@
"Blog Type": "部落格類型", "Blog Type": "部落格類型",
"Blog Address": "部落格網址", "Blog Address": "部落格網址",
"Save": "儲存", "Save": "儲存",
"Auth": "Auth", "Auth": "驗證",
"Authentication Method": "認證方法", "Authentication Method": "認證方法",
"JWT": "JWT", "JWT": "JWT",
"USER": "USER", "USER": "USER",
"Token": "Token", "Token": "Token",
"Storage": "本機儲存空間", "Storage": "儲存空間",
"Hotkeys": "快捷鍵", "Hotkeys": "快捷鍵",
"Show/Hide Boostnote": "顯示/隱藏 Boostnote", "Show/Hide Boostnote": "顯示/隱藏 Boostnote",
"Restore": "還原", "Restore": "還原",
@@ -144,7 +144,7 @@
"Russian": "Russian", "Russian": "Russian",
"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 標籤 (建議)",
"Allow styles": "允許樣式", "Allow styles": "允許樣式",

35
package-lock.json generated
View File

@@ -1,35 +0,0 @@
{
"name": "boost",
"version": "0.10.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"i18n-2": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/i18n-2/-/i18n-2-0.7.2.tgz",
"integrity": "sha512-Rdh6vfpNhL7q61cNf27x7QGULTi1TcGLVdFb5OJ6dOiJo+EkOTqEg0+3xgyeEMgYhopUBsh2IiSkFkjM+EhEmA==",
"requires": {
"debug": "3.1.0",
"sprintf": "0.1.5"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"sprintf": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/sprintf/-/sprintf-0.1.5.tgz",
"integrity": "sha1-j4PjmpMXwaUCy324BQ5Rxnn27c8="
}
}
}

View File

@@ -1,23 +1,21 @@
{ {
"name": "boost", "name": "boost",
"productName": "Boostnote", "productName": "Boostnote",
"version": "0.11.6", "version": "0.11.9",
"main": "index.js", "main": "index.js",
"description": "Boostnote", "description": "Boostnote",
"license": "GPL-3.0", "license": "GPL-3.0",
"scripts": { "scripts": {
"start": "electron ./index.js", "start": "electron ./index.js",
"hot": "electron ./index.js --hot",
"webpack": "webpack-dev-server --hot --inline --config webpack.config.js",
"compile": "grunt compile", "compile": "grunt compile",
"test": "PWD=$(pwd) NODE_ENV=test ava --serial", "test": "PWD=$(pwd) NODE_ENV=test ava --serial",
"jest": "jest", "jest": "jest",
"fix": "eslint . --fix", "fix": "eslint . --fix",
"lint": "eslint .", "lint": "eslint .",
"dev-start": "concurrently --kill-others \"npm run webpack\" \"npm run hot\"" "dev": "node dev-scripts/dev.js"
}, },
"config": { "config": {
"electron-version": "1.8.7" "electron-version": "2.0.7"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -51,13 +49,16 @@
"dependencies": { "dependencies": {
"@rokt33r/markdown-it-math": "^4.0.1", "@rokt33r/markdown-it-math": "^4.0.1",
"@rokt33r/season": "^5.3.0", "@rokt33r/season": "^5.3.0",
"@susisu/mte-kernel": "^2.0.0",
"aws-sdk": "^2.48.0", "aws-sdk": "^2.48.0",
"aws-sdk-mobile-analytics": "^0.9.2", "aws-sdk-mobile-analytics": "^0.9.2",
"codemirror": "^5.37.0", "chart.js": "^2.7.2",
"codemirror": "^5.39.0",
"codemirror-mode-elixir": "^1.1.1", "codemirror-mode-elixir": "^1.1.1",
"electron-config": "^0.2.1", "electron-config": "^1.0.0",
"electron-gh-releases": "^2.0.2", "electron-gh-releases": "^2.0.2",
"escape-string-regexp": "^1.0.5", "escape-string-regexp": "^1.0.5",
"file-url": "^2.0.2",
"filenamify": "^2.0.0", "filenamify": "^2.0.0",
"flowchart.js": "^1.6.5", "flowchart.js": "^1.6.5",
"font-awesome": "^4.3.0", "font-awesome": "^4.3.0",
@@ -70,18 +71,17 @@
"lodash": "^4.11.1", "lodash": "^4.11.1",
"lodash-move": "^1.1.1", "lodash-move": "^1.1.1",
"markdown-it": "^6.0.1", "markdown-it": "^6.0.1",
"markdown-it-admonition": "https://github.com/johannbre/markdown-it-admonition.git", "markdown-it-admonition": "^1.0.4",
"markdown-it-checkbox": "^1.1.0",
"markdown-it-emoji": "^1.1.1", "markdown-it-emoji": "^1.1.1",
"markdown-it-footnote": "^3.0.0", "markdown-it-footnote": "^3.0.0",
"markdown-it-imsize": "^2.0.1", "markdown-it-imsize": "^2.0.1",
"markdown-it-kbd": "^1.1.1", "markdown-it-kbd": "^1.1.1",
"markdown-it-multimd-table": "^2.0.1", "markdown-it-multimd-table": "^2.0.1",
"markdown-it-named-headers": "^0.0.4", "markdown-it-named-headers": "^0.0.4",
"markdown-it-plantuml": "^0.3.0", "markdown-it-plantuml": "^1.1.0",
"markdown-it-smartarrows": "^1.0.1", "markdown-it-smartarrows": "^1.0.1",
"md5": "^2.0.0",
"mdurl": "^1.0.1", "mdurl": "^1.0.1",
"mermaid": "^8.0.0-rc.8",
"moment": "^2.10.3", "moment": "^2.10.3",
"mousetrap": "^1.6.1", "mousetrap": "^1.6.1",
"mousetrap-global-bind": "^1.1.0", "mousetrap-global-bind": "^1.1.0",
@@ -97,8 +97,6 @@
"sander": "^0.5.1", "sander": "^0.5.1",
"sanitize-html": "^1.18.2", "sanitize-html": "^1.18.2",
"striptags": "^2.2.1", "striptags": "^2.2.1",
"superagent": "^1.2.0",
"superagent-promise": "^1.0.3",
"unique-slug": "2.0.0", "unique-slug": "2.0.0",
"uuid": "^3.2.1" "uuid": "^3.2.1"
}, },
@@ -120,8 +118,8 @@
"css-loader": "^0.19.0", "css-loader": "^0.19.0",
"devtron": "^1.1.0", "devtron": "^1.1.0",
"dom-storage": "^2.0.2", "dom-storage": "^2.0.2",
"electron": "2.0.2", "electron": "2.0.7",
"electron-packager": "^6.0.0", "electron-packager": "^12.0.0",
"eslint": "^3.13.1", "eslint": "^3.13.1",
"eslint-config-standard": "^6.2.1", "eslint-config-standard": "^6.2.1",
"eslint-config-standard-jsx": "^3.2.0", "eslint-config-standard-jsx": "^3.2.0",
@@ -145,6 +143,7 @@
"react-router": "^2.4.0", "react-router": "^2.4.0",
"react-router-redux": "^4.0.4", "react-router-redux": "^4.0.4",
"react-test-renderer": "^15.6.2", "react-test-renderer": "^15.6.2",
"signale": "^1.2.1",
"standard": "^8.4.0", "standard": "^8.4.0",
"style-loader": "^0.12.4", "style-loader": "^0.12.4",
"stylus": "^0.52.4", "stylus": "^0.52.4",

View File

@@ -1,4 +1,4 @@
:mega: We've launched [Boostnote Bounty Program](http://bit.ly/2I5Tpik). :mega: The Boostnote team launches [IssueHunt](https://issuehunt.io/) for sustainable open-source ecosystem.
![Boostnote app screenshot](./resources/repository/top.png) ![Boostnote app screenshot](./resources/repository/top.png)
@@ -19,13 +19,15 @@ Thank you to all the people who already 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 entirely thanks to the support by these awesome [backers](https://github.com/BoostIO/Boostnote/blob/master/Backers.md). If you'd like to join them, please consider: Boostnote is an open source project. It's an independent project with its ongoing development made possible entirely thanks to the support by these awesome backers.
- [Become a backer or sponsor on Open Collective.](https://opencollective.com/boostnoteio)
Any issues on Boostnote can be funded by anyone and that money will be distributed to contributors and maintainers. If you'd like to join them, please consider:
- [Become a backer on IssueHunt](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/enQtMzcwNDU3NDU3ODI0LTU1ZDgwZDNiZTNmN2RhOTY4OTM5ODY0ODUzMTRiNmQ0ZDMzZDRiYzg2YmQ5ZDYzZTQxYjMxYzBlNTM4NjcyYjM) - [Slack Group](https://join.slack.com/t/boostnote-group/shared_invite/enQtMzkxOTk4ODkyNzc0LThkNmMzY2VlZjVhYTNiYjE5YjQyZGVjNTJlYTY1OGMyZTFjNGU5YTUyYjUzOWZhYTU4OTVlNDYyNDFjYWMzNDM)
- [Blog](https://boostlog.io/tags/boostnote) - [Blog](https://boostlog.io/tags/boostnote)
- [Reddit](https://www.reddit.com/r/Boostnote/) - [Reddit](https://www.reddit.com/r/Boostnote/)

View File

@@ -169,6 +169,43 @@ it('should replace the all ":storage" path with the actual storage path', functi
expect(actual).toEqual(expectedOutput) expect(actual).toEqual(expectedOutput)
}) })
it('should replace the ":storage" path with the actual storage path when they have different path separators', function () {
const storageFolder = systemUnderTest.DESTINATION_FOLDER
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.win32.sep) + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
' </p>\n' +
' <p data-line="4">\n' +
' <a href=":storage' + mdurl.encode(path.posix.sep) + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
' </p>\n' +
' </body>\n' +
'</html>'
const storagePath = '<<dummyStoragePath>>'
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="file:///' + storagePath + path.sep + storageFolder + path.sep + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
' </p>\n' +
' <p data-line="4">\n' +
' <a href="file:///' + storagePath + path.sep + storageFolder + path.sep + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
' </p>\n' +
' </body>\n' +
'</html>'
const actual = systemUnderTest.fixLocalURLS(testInput, storagePath)
expect(actual).toEqual(expectedOutput)
})
it('should test that generateAttachmentMarkdown works correct both with previews and without', function () { it('should test that generateAttachmentMarkdown works correct both with previews and without', function () {
const fileName = 'fileName' const fileName = 'fileName'
const path = 'path' const path = 'path'
@@ -180,27 +217,35 @@ it('should test that generateAttachmentMarkdown works correct both with previews
expect(actual).toEqual(expected) expect(actual).toEqual(expected)
}) })
it('should test that getAttachmentsInContent finds all attachments', function () { it('should test that migrateAttachments work when they have different path separators', function () {
const testInput = sander.existsSync = jest.fn(() => true)
'<html>\n' + const dummyStoragePath = 'dummyStoragePath'
' <head>\n' + const imagesPath = path.join(dummyStoragePath, 'images')
' //header\n' + const attachmentsPath = path.join(dummyStoragePath, 'attachments')
' </head>\n' + const noteKey = 'noteKey'
' <body data-theme="default">\n' + const testInput = '"# Test\n' +
' <h2 data-line="0" id="Headline">Headline</h2>\n' + '\n' +
' <p data-line="2">\n' + '![Screenshot1](:storage' + path.win32.sep + '0.3b88d0dc.png)\n' +
' <img src=":storage' + mdurl.encode(path.sep) + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + mdurl.encode(path.sep) + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' + '![Screenshot2](:storage' + path.posix.sep + '0.2cb8875c.pdf)"'
' </p>\n' +
' <p data-line="4">\n' + systemUnderTest.migrateAttachments(testInput, dummyStoragePath, noteKey)
' <a href=":storage' + mdurl.encode(path.sep) + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + mdurl.encode(path.sep) + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
' </p>\n' + expect(sander.existsSync.mock.calls[0][0]).toBe(imagesPath)
' <p data-line="6">\n' + expect(sander.existsSync.mock.calls[1][0]).toBe(path.join(imagesPath, '0.3b88d0dc.png'))
' <img src=":storage' + mdurl.encode(path.sep) + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + mdurl.encode(path.sep) + 'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' + expect(sander.existsSync.mock.calls[2][0]).toBe(path.join(attachmentsPath, '0.3b88d0dc.png'))
' </p>\n' + expect(sander.existsSync.mock.calls[3][0]).toBe(path.join(imagesPath, '0.2cb8875c.pdf'))
' </body>\n' + expect(sander.existsSync.mock.calls[4][0]).toBe(path.join(attachmentsPath, '0.2cb8875c.pdf'))
'</html>' })
const actual = systemUnderTest.getAttachmentsInContent(testInput)
const expected = [':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg'] it('should test that getAttachmentsInMarkdownContent finds all attachments when they have different path separators', function () {
const testInput = '"# Test\n' +
'\n' +
'![Screenshot1](:storage' + path.win32.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.win32.sep + '0.3b88d0dc.png)\n' +
'![Screenshot2](:storage' + path.posix.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.posix.sep + '2cb8875c.pdf)\n' +
'![Screenshot3](:storage' + path.win32.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.posix.sep + 'bbf49b02.jpg)"'
const actual = systemUnderTest.getAttachmentsInMarkdownContent(testInput)
const expected = [':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.3b88d0dc.png', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '2cb8875c.pdf', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'bbf49b02.jpg']
expect(actual).toEqual(expect.arrayContaining(expected)) expect(actual).toEqual(expect.arrayContaining(expected))
}) })
@@ -225,8 +270,8 @@ it('should test that getAbsolutePathsOfAttachmentsInContent returns all absolute
' </body>\n' + ' </body>\n' +
'</html>' '</html>'
const actual = systemUnderTest.getAbsolutePathsOfAttachmentsInContent(testInput, dummyStoragePath) const actual = systemUnderTest.getAbsolutePathsOfAttachmentsInContent(testInput, dummyStoragePath)
const expected = [dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp', const expected = [dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp.png',
dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx', dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx.pdf',
dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg'] dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg']
expect(actual).toEqual(expect.arrayContaining(expected)) expect(actual).toEqual(expect.arrayContaining(expected))
}) })
@@ -274,6 +319,21 @@ it('should remove the all ":storage" and noteKey references', function () {
expect(actual).toEqual(expectedOutput) expect(actual).toEqual(expectedOutput)
}) })
it('should make sure that "removeStorageAndNoteReferences" works with markdown content as well', function () {
const noteKey = 'noteKey'
const testInput =
'Test input' +
'![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + noteKey + path.sep + 'image.jpg](imageName}) \n' +
'[' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + noteKey + path.sep + 'pdf.pdf](pdf})'
const expectedOutput =
'Test input' +
'![' + systemUnderTest.DESTINATION_FOLDER + path.sep + 'image.jpg](imageName}) \n' +
'[' + systemUnderTest.DESTINATION_FOLDER + path.sep + 'pdf.pdf](pdf})'
const actual = systemUnderTest.removeStorageAndNoteReferences(testInput, noteKey)
expect(actual).toEqual(expectedOutput)
})
it('should delete the correct attachment folder if a note is deleted', function () { it('should delete the correct attachment folder if a note is deleted', function () {
const dummyStorage = {path: 'dummyStoragePath'} const dummyStorage = {path: 'dummyStoragePath'}
const storageKey = 'storageKey' const storageKey = 'storageKey'

View File

@@ -0,0 +1,38 @@
const test = require('ava')
const toggleStorage = require('browser/main/lib/dataApi/toggleStorage')
global.document = require('jsdom').jsdom('<body></body>')
global.window = document.defaultView
global.navigator = window.navigator
const Storage = require('dom-storage')
const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true })
const path = require('path')
const _ = require('lodash')
const TestDummy = require('../fixtures/TestDummy')
const sander = require('sander')
const os = require('os')
const storagePath = path.join(os.tmpdir(), 'test/toggle-storage')
test.beforeEach((t) => {
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
})
test.serial('Toggle a storage location', (t) => {
const storageKey = t.context.storage.cache.key
return Promise.resolve()
.then(function doTest () {
return toggleStorage(storageKey, true)
})
.then(function assert (data) {
const cachedStorageList = JSON.parse(localStorage.getItem('storages'))
t.true(_.find(cachedStorageList, {key: storageKey}).isOpen === true)
})
})
test.after(function after () {
localStorage.clear()
sander.rimrafSync(storagePath)
})

View File

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

View File

@@ -12,6 +12,7 @@ test('getTodoStatus should return a correct hash object', t => {
['- [ ] a\n- [x] a\n', { total: 2, completed: 1 }], ['- [ ] a\n- [x] a\n', { total: 2, completed: 1 }],
['+ [ ] a\n', { total: 1, completed: 0 }], ['+ [ ] a\n', { total: 1, completed: 0 }],
['+ [ ] a\n+ [x] a\n', { total: 2, completed: 1 }], ['+ [ ] a\n+ [x] a\n', { total: 2, completed: 1 }],
['+ [ ] a\n+ [X] a\n', { total: 2, completed: 1 }],
['+ [ ] a\n+ [testx] a\n', { total: 1, completed: 0 }], ['+ [ ] a\n+ [testx] a\n', { total: 1, completed: 0 }],
['+ [ ] a\n+ [xtest] a\n', { total: 1, completed: 0 }], ['+ [ ] a\n+ [xtest] a\n', { total: 1, completed: 0 }],
['+ [ ] a\n+ foo[x]bar a\n', { total: 1, completed: 0 }], ['+ [ ] a\n+ foo[x]bar a\n', { total: 1, completed: 0 }],

View File

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

View File

@@ -5,7 +5,7 @@ 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', t => {
const validJson = { 'editor': { 'keyMap': 'vim', 'switchPreview': 'BLUR', 'theme': 'monokai' }, 'hotkey': { 'toggleMain': 'Control + L' }, 'listWidth': 135, 'navWidth': 135 } const validJson = { 'editor': { 'keyMap': 'vim', 'switchPreview': 'BLUR', 'theme': 'monokai' }, 'hotkey': { 'toggleMain': 'Control + L' }, 'listWidth': 135, 'navWidth': 135 }
const allJson = { 'amaEnabled': true, 'editor': { 'fontFamily': 'Monaco, Consolas', 'fontSize': '14', 'indentSize': '2', 'indentType': 'space', 'keyMap': 'vim', 'switchPreview': 'BLUR', 'theme': 'monokai' }, 'hotkey': { 'toggleMain': 'Cmd + Alt + L' }, 'isSideNavFolded': false, 'listStyle': 'DEFAULT', 'listWidth': 174, 'navWidth': 200, 'preview': { 'codeBlockTheme': 'dracula', 'fontFamily': 'Lato', 'fontSize': '14', 'lineNumber': true }, 'sortBy': 'UPDATED_AT', 'ui': { 'defaultNote': 'ALWAYS_ASK', 'disableDirectWrite': false, 'theme': 'default' }, 'zoom': 1 } const allJson = { 'amaEnabled': true, 'editor': { 'fontFamily': 'Monaco, Consolas', 'fontSize': '14', 'indentSize': '2', 'indentType': 'space', 'keyMap': 'vim', 'switchPreview': 'BLUR', 'theme': 'monokai' }, 'hotkey': { 'toggleMain': 'Cmd + Alt + L' }, 'isSideNavFolded': false, 'listStyle': 'DEFAULT', 'listWidth': 174, 'navWidth': 200, 'preview': { 'codeBlockTheme': 'dracula', 'fontFamily': 'Lato', 'fontSize': '14', 'lineNumber': true }, 'sortBy': { 'default': 'UPDATED_AT' }, 'ui': { 'defaultNote': 'ALWAYS_ASK', 'disableDirectWrite': false, 'theme': 'default' }, 'zoom': 1 }
// [input, expected] // [input, expected]
const validTestCases = [ const validTestCases = [

View File

@@ -4,20 +4,24 @@ const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin')
var config = { var config = {
entry: { entry: {
main: './browser/main/index.js' main: ['./browser/main/index.js']
}, },
resolve: { resolve: {
extensions: ['', '.js', '.jsx', '.styl'], extensions: ['', '.js', '.jsx', '.styl'],
packageMains: ['webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main'], packageMains: [
'webpack',
'browser',
'web',
'browserify',
['jam', 'main'],
'main'
],
alias: { alias: {
'lib': path.join(__dirname, 'lib'), lib: path.join(__dirname, 'lib'),
'browser': path.join(__dirname, 'browser') browser: path.join(__dirname, 'browser')
} }
}, },
plugins: [ plugins: [new webpack.NoErrorsPlugin(), new NodeTargetPlugin()],
new webpack.NoErrorsPlugin(),
new NodeTargetPlugin()
],
stylus: { stylus: {
use: [require('nib')()], use: [require('nib')()],
import: [ import: [
@@ -28,16 +32,12 @@ var config = {
externals: [ externals: [
'node-ipc', 'node-ipc',
'electron', 'electron',
'md5',
'superagent',
'superagent-promise',
'lodash', 'lodash',
'markdown-it', 'markdown-it',
'moment', 'moment',
'markdown-it-emoji', 'markdown-it-emoji',
'fs-jetpack', 'fs-jetpack',
'@rokt33r/markdown-it-math', '@rokt33r/markdown-it-math',
'markdown-it-checkbox',
'markdown-it-kbd', 'markdown-it-kbd',
'markdown-it-plantuml', 'markdown-it-plantuml',
'markdown-it-admonition', 'markdown-it-admonition',
@@ -47,14 +47,13 @@ var config = {
react: 'var React', react: 'var React',
'react-dom': 'var ReactDOM', 'react-dom': 'var ReactDOM',
'react-redux': 'var ReactRedux', 'react-redux': 'var ReactRedux',
'codemirror': 'var CodeMirror', codemirror: 'var CodeMirror',
'redux': 'var Redux', redux: 'var Redux',
'raphael': 'var Raphael', raphael: 'var Raphael',
'flowchart': 'var flowchart', flowchart: 'var flowchart',
'sequence-diagram': 'var Diagram' 'sequence-diagram': 'var Diagram'
} }
] ]
} }
module.exports = config module.exports = config

799
yarn.lock

File diff suppressed because it is too large Load Diff