mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 22:41:53 +08:00
Add experimental spellchecker (#1424)
* Experimental spellchecker for testing purpose * Fix 'apache' license validation * Use local electron-spellchecker for development * Add settings and bug fixes * Fix Hunspell switchLanguage bug and improvements * Fix attach to editor when enabling spell check again * Add Hunspell license * Copy default Huspell dictionary on first start * Fix full language name * Some code improvements * Allow to add words to user dict and bug fixes * Allow to change Muya's spellcheck container attribute * feat: Don't underline misspelled words * Allow to set Hunspell on macOS * Fix spellchecker changed value * Refactor switchLanguage, init and enableSpellchecker * Refactor and some fixes * Code improvements * electron-spellchecker cleanup and optimization * Disable automatic language detection for Hunspell * Fix init on macOS and update JSDoc * Fix macOS issues and some improvements * Load single settings value only * Fix rebase * Remove debug code * Move electron-spellchecker to scoped npm repo * Fix dictionary of ignored words on macOS * Move replaceWordInline to core API * Remove comment block * Fix upstream lint error
This commit is contained in:
parent
1b9ee786be
commit
603ed04ab1
@ -10,7 +10,7 @@ const getLicenses = (rootDir, callback) => {
|
|||||||
direct: true,
|
direct: true,
|
||||||
excludePackages: 'xmldom@0.1.27', // xmldom@0.1.27 is under MIT License, but license-checker show it's under LGPL License.
|
excludePackages: 'xmldom@0.1.27', // xmldom@0.1.27 is under MIT License, but license-checker show it's under LGPL License.
|
||||||
json: true,
|
json: true,
|
||||||
onlyAllow: 'Unlicense;WTFPL;ISC;MIT;BSD;ISC;Apache-2.0;MIT*;Apache*;BSD*;CC0-1.0;CC-BY-4.0;CC-BY-3.0',
|
onlyAllow: 'Unlicense;WTFPL;ISC;MIT;BSD;ISC;Apache-2.0;MIT*;Apache;Apache*;BSD*;CC0-1.0;CC-BY-4.0;CC-BY-3.0',
|
||||||
customPath: {
|
customPath: {
|
||||||
licenses: '',
|
licenses: '',
|
||||||
licenseText: 'none'
|
licenseText: 'none'
|
||||||
|
17
electron-builder.yml
Normal file → Executable file
17
electron-builder.yml
Normal file → Executable file
@ -18,14 +18,31 @@ files:
|
|||||||
- "!node_modules/terser/dist/bundle.min.js.map"
|
- "!node_modules/terser/dist/bundle.min.js.map"
|
||||||
- "!node_modules/vega-lite/build/vega-lite*.js.map"
|
- "!node_modules/vega-lite/build/vega-lite*.js.map"
|
||||||
# Don't bundle build files
|
# Don't bundle build files
|
||||||
|
- "!node_modules/@felixrieseberg/spellchecker/bin"
|
||||||
|
- "!node_modules/ced/bin"
|
||||||
|
- "!node_modules/ced/vendor"
|
||||||
|
- "!node_modules/cld/bin"
|
||||||
|
- "!node_modules/cld/deps"
|
||||||
- "!node_modules/fontmanager-redux/bin"
|
- "!node_modules/fontmanager-redux/bin"
|
||||||
- "!node_modules/keyboard-layout/bin"
|
- "!node_modules/keyboard-layout/bin"
|
||||||
- "!node_modules/keytar/bin"
|
- "!node_modules/keytar/bin"
|
||||||
- "!node_modules/vscode-windows-registry/bin"
|
- "!node_modules/vscode-windows-registry/bin"
|
||||||
|
# Don't bundle Windows build files
|
||||||
|
- "!node_modules/**/{*.vcxproj,*.vcxproj.filters}"
|
||||||
|
- "!node_modules/**/build/Release/{*.lib,*.exp,*.ilk,*.pdb}"
|
||||||
|
- "!node_modules/**/build/Release/obj"
|
||||||
|
- "!node_modules/ced/build/vendor"
|
||||||
|
# Don't bundle LGPL source files
|
||||||
|
- "!node_modules/@felixrieseberg/spellchecker/vendor"
|
||||||
extraFiles:
|
extraFiles:
|
||||||
- "LICENSE"
|
- "LICENSE"
|
||||||
- from: "resources/THIRD-PARTY-LICENSES.txt"
|
- from: "resources/THIRD-PARTY-LICENSES.txt"
|
||||||
to: "THIRD-PARTY-LICENSES.txt"
|
to: "THIRD-PARTY-LICENSES.txt"
|
||||||
|
extraResources:
|
||||||
|
- from: "resources/hunspell_dictionaries/"
|
||||||
|
to: "hunspell_dictionaries/"
|
||||||
|
filter:
|
||||||
|
- "!**/LICENSE-hunspell.txt"
|
||||||
|
|
||||||
fileAssociations:
|
fileAssociations:
|
||||||
- ext:
|
- ext:
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
"build:bin": "node .electron-vue/build.js && electron-builder --dir",
|
"build:bin": "node .electron-vue/build.js && electron-builder --dir",
|
||||||
"build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js",
|
"build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js",
|
||||||
"build:dev": "node .electron-vue/build.js",
|
"build:dev": "node .electron-vue/build.js",
|
||||||
"dev": "node .electron-vue/dev-runner.js",
|
"dev": "cross-env node .electron-vue/dev-runner.js",
|
||||||
"e2e": "yarn run pack && cross-env MARKTEXT_EXIT_ON_ERROR=1 mocha --timeout 10000 test/e2e",
|
"e2e": "yarn run pack && cross-env MARKTEXT_EXIT_ON_ERROR=1 mocha --timeout 10000 test/e2e",
|
||||||
"lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter src test",
|
"lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter src test",
|
||||||
"lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src test",
|
"lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src test",
|
||||||
@ -28,12 +28,13 @@
|
|||||||
"preinstall": "node .electron-vue/preinstall.js",
|
"preinstall": "node .electron-vue/preinstall.js",
|
||||||
"build:muya": "cd src/muya && webpack --progress --colors --config webpack.config.js",
|
"build:muya": "cd src/muya && webpack --progress --colors --config webpack.config.js",
|
||||||
"release:muya": "yarn run build:muya && cd src/muya && yarn publish",
|
"release:muya": "yarn run build:muya && cd src/muya && yarn publish",
|
||||||
"rebuild": "electron-rebuild -f -o fontmanager-redux,keytar,keyboard-layout,vscode-windows-registry",
|
"rebuild": "electron-rebuild -f",
|
||||||
"gen-third-party": "node tools/generateThirdPartyLicense.js",
|
"gen-third-party": "node tools/generateThirdPartyLicense.js",
|
||||||
"validate-licenses": "node tools/validateLicenses.js"
|
"validate-licenses": "node tools/validateLicenses.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hfelix/electron-localshortcut": "^3.1.1",
|
"@hfelix/electron-localshortcut": "^3.1.1",
|
||||||
|
"@hfelix/electron-spellchecker": "^1.0.0-rc.1",
|
||||||
"@octokit/rest": "^16.33.1",
|
"@octokit/rest": "^16.33.1",
|
||||||
"arg": "^4.1.1",
|
"arg": "^4.1.1",
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
@ -60,6 +61,7 @@
|
|||||||
"github-markdown-css": "^3.0.1",
|
"github-markdown-css": "^3.0.1",
|
||||||
"html-tags": "^3.0.0",
|
"html-tags": "^3.0.0",
|
||||||
"iconv-lite": "^0.5.0",
|
"iconv-lite": "^0.5.0",
|
||||||
|
"iso-639-1": "^2.1.0",
|
||||||
"joplin-turndown-plugin-gfm": "^1.0.11",
|
"joplin-turndown-plugin-gfm": "^1.0.11",
|
||||||
"katex": "^0.11.1",
|
"katex": "^0.11.1",
|
||||||
"keyboard-layout": "^2.0.16",
|
"keyboard-layout": "^2.0.16",
|
||||||
|
File diff suppressed because it is too large
Load Diff
273
resources/hunspell_dictionaries/LICENSE-en-US.txt
Normal file
273
resources/hunspell_dictionaries/LICENSE-en-US.txt
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
en_US Hunspell Dictionary
|
||||||
|
Version 2017.01.22
|
||||||
|
Sun Jan 22 17:22:40 2017 -0500 [0c64f5c]
|
||||||
|
http://wordlist.sourceforge.net
|
||||||
|
|
||||||
|
README file for English Hunspell dictionaries derived from SCOWL.
|
||||||
|
These dictionaries are created using the speller/make-hunspell-dict
|
||||||
|
script in SCOWL.
|
||||||
|
|
||||||
|
The following dictionaries are available:
|
||||||
|
en_US (American)
|
||||||
|
en_CA (Canadian)
|
||||||
|
en_GB-ise (British with "ise" spelling)
|
||||||
|
en_GB-ize (British with "ize" spelling)
|
||||||
|
en_AU (Australian)
|
||||||
|
en_US-large
|
||||||
|
en_CA-large
|
||||||
|
en_GB-large (with both "ise" and "ize" spelling)
|
||||||
|
en_AU-large
|
||||||
|
|
||||||
|
The normal (non-large) dictionaries correspond to SCOWL size 60 and,
|
||||||
|
to encourage consistent spelling, generally only include one spelling
|
||||||
|
variant for a word. The large dictionaries correspond to SCOWL size
|
||||||
|
70 and may include multiple spelling for a word when both variants are
|
||||||
|
considered almost equal. The larger dictionaries however (1) have not
|
||||||
|
been as carefully checked for errors as the normal dictionaries and
|
||||||
|
thus may contain misspelled or invalid words; and (2) contain
|
||||||
|
uncommon, yet valid, words that might cause problems as they are
|
||||||
|
likely to be misspellings of more common words (for example, "ort" and
|
||||||
|
"calender").
|
||||||
|
To get an idea of the difference in size, here are 25 random words
|
||||||
|
only found in the large dictionary for American English:
|
||||||
|
Bermejo Freyr's Guenevere Hatshepsut Nottinghamshire arrestment
|
||||||
|
crassitudes crural dogwatches errorless fetial flaxseeds godroon
|
||||||
|
incretion jalapeño's kelpie kishkes neuroglias pietisms pullulation
|
||||||
|
stemwinder stenoses syce thalassic zees
|
||||||
|
The en_US, en_CA and en_AU are the official dictionaries for Hunspell.
|
||||||
|
The en_GB and large dictionaries are made available on an experimental
|
||||||
|
basis. If you find them useful please send me a quick email at
|
||||||
|
kevina@gnu.org.
|
||||||
|
If none of these dictionaries suite you (for example, maybe you want
|
||||||
|
the normal dictionary that also includes common variants) additional
|
||||||
|
dictionaries can be generated at http://app.aspell.net/create or by
|
||||||
|
modifying speller/make-hunspell-dict in SCOWL. Please do let me know
|
||||||
|
if you end up publishing a customized dictionary.
|
||||||
|
If a word is not found in the dictionary or a word is there you think
|
||||||
|
shouldn't be, you can lookup the word up at http://app.aspell.net/lookup
|
||||||
|
to help determine why that is.
|
||||||
|
General comments on these list can be sent directly to me at
|
||||||
|
kevina@gnu.org or to the wordlist-devel mailing lists
|
||||||
|
(https://lists.sourceforge.net/lists/listinfo/wordlist-devel). If you
|
||||||
|
have specific issues with any of these dictionaries please file a bug
|
||||||
|
report at https://github.com/kevina/wordlist/issues.
|
||||||
|
IMPORTANT CHANGES INTRODUCED In 2016.11.20:
|
||||||
|
New Australian dictionaries thanks to the work of Benjamin Titze
|
||||||
|
(btitze@protonmail.ch).
|
||||||
|
IMPORTANT CHANGES INTRODUCED IN 2016.04.24:
|
||||||
|
The dictionaries are now in UTF-8 format instead of ISO-8859-1. This
|
||||||
|
was required to handle smart quotes correctly.
|
||||||
|
IMPORTANT CHANGES INTRODUCED IN 2016.01.19:
|
||||||
|
"SET UTF8" was changes to "SET UTF-8" in the affix file as some
|
||||||
|
versions of Hunspell do not recognize "UTF8".
|
||||||
|
ADDITIONAL NOTES:
|
||||||
|
The NOSUGGEST flag was added to certain taboo words. While I made an
|
||||||
|
honest attempt to flag the strongest taboo words with the NOSUGGEST
|
||||||
|
flag, I MAKE NO GUARANTEE THAT I FLAGGED EVERY POSSIBLE TABOO WORD.
|
||||||
|
The list was originally derived from Németh László, however I removed
|
||||||
|
some words which, while being considered taboo by some dictionaries,
|
||||||
|
are not really considered swear words in today's society.
|
||||||
|
COPYRIGHT, SOURCES, and CREDITS:
|
||||||
|
The English dictionaries come directly from SCOWL
|
||||||
|
and is thus under the same copyright of SCOWL. The affix file is
|
||||||
|
a heavily modified version of the original english.aff file which was
|
||||||
|
released as part of Geoff Kuenning's Ispell and as such is covered by
|
||||||
|
his BSD license. Part of SCOWL is also based on Ispell thus the
|
||||||
|
Ispell copyright is included with the SCOWL copyright.
|
||||||
|
The collective work is Copyright 2000-2016 by Kevin Atkinson as well
|
||||||
|
as any of the copyrights mentioned below:
|
||||||
|
Copyright 2000-2016 by Kevin Atkinson
|
||||||
|
Permission to use, copy, modify, distribute and sell these word
|
||||||
|
lists, the associated scripts, the output created from the scripts,
|
||||||
|
and its documentation for any purpose is hereby granted without fee,
|
||||||
|
provided that the above copyright notice appears in all copies and
|
||||||
|
that both that copyright notice and this permission notice appear in
|
||||||
|
supporting documentation. Kevin Atkinson makes no representations
|
||||||
|
about the suitability of this array for any purpose. It is provided
|
||||||
|
"as is" without express or implied warranty.
|
||||||
|
Alan Beale <biljir@pobox.com> also deserves special credit as he has,
|
||||||
|
in addition to providing the 12Dicts package and being a major
|
||||||
|
contributor to the ENABLE word list, given me an incredible amount of
|
||||||
|
feedback and created a number of special lists (those found in the
|
||||||
|
Supplement) in order to help improve the overall quality of SCOWL.
|
||||||
|
The 10 level includes the 1000 most common English words (according to
|
||||||
|
the Moby (TM) Words II [MWords] package), a subset of the 1000 most
|
||||||
|
common words on the Internet (again, according to Moby Words II), and
|
||||||
|
frequently class 16 from Brian Kelk's "UK English Wordlist
|
||||||
|
with Frequency Classification".
|
||||||
|
The MWords package was explicitly placed in the public domain:
|
||||||
|
The Moby lexicon project is complete and has
|
||||||
|
been place into the public domain. Use, sell,
|
||||||
|
rework, excerpt and use in any way on any platform.
|
||||||
|
Placing this material on internal or public servers is
|
||||||
|
also encouraged. The compiler is not aware of any
|
||||||
|
export restrictions so freely distribute world-wide.
|
||||||
|
You can verify the public domain status by contacting
|
||||||
|
Grady Ward
|
||||||
|
3449 Martha Ct.
|
||||||
|
Arcata, CA 95521-4884
|
||||||
|
grady@netcom.com
|
||||||
|
grady@northcoast.com
|
||||||
|
The "UK English Wordlist With Frequency Classification" is also in the
|
||||||
|
Public Domain:
|
||||||
|
Date: Sat, 08 Jul 2000 20:27:21 +0100
|
||||||
|
From: Brian Kelk <Brian.Kelk@cl.cam.ac.uk>
|
||||||
|
> I was wondering what the copyright status of your "UK English
|
||||||
|
> Wordlist With Frequency Classification" word list as it seems to
|
||||||
|
> be lacking any copyright notice.
|
||||||
|
There were many many sources in total, but any text marked
|
||||||
|
"copyright" was avoided. Locally-written documentation was one
|
||||||
|
source. An earlier version of the list resided in a filespace called
|
||||||
|
PUBLIC on the University mainframe, because it was considered public
|
||||||
|
domain.
|
||||||
|
Date: Tue, 11 Jul 2000 19:31:34 +0100
|
||||||
|
> So are you saying your word list is also in the public domain?
|
||||||
|
That is the intention.
|
||||||
|
The 20 level includes frequency classes 7-15 from Brian's word list.
|
||||||
|
The 35 level includes frequency classes 2-6 and words appearing in at
|
||||||
|
least 11 of 12 dictionaries as indicated in the 12Dicts package. All
|
||||||
|
words from the 12Dicts package have had likely inflections added via
|
||||||
|
my inflection database.
|
||||||
|
The 12Dicts package and Supplement is in the Public Domain.
|
||||||
|
The WordNet database, which was used in the creation of the
|
||||||
|
Inflections database, is under the following copyright:
|
||||||
|
This software and database is being provided to you, the LICENSEE,
|
||||||
|
by Princeton University under the following license. By obtaining,
|
||||||
|
using and/or copying this software and database, you agree that you
|
||||||
|
have read, understood, and will comply with these terms and
|
||||||
|
conditions.:
|
||||||
|
Permission to use, copy, modify and distribute this software and
|
||||||
|
database and its documentation for any purpose and without fee or
|
||||||
|
royalty is hereby granted, provided that you agree to comply with
|
||||||
|
the following copyright notice and statements, including the
|
||||||
|
disclaimer, and that the same appear on ALL copies of the software,
|
||||||
|
database and documentation, including modifications that you make
|
||||||
|
for internal use or for distribution.
|
||||||
|
WordNet 1.6 Copyright 1997 by Princeton University. All rights
|
||||||
|
reserved.
|
||||||
|
THIS SOFTWARE AND DATABASE IS PROVIDED "AS IS" AND PRINCETON
|
||||||
|
UNIVERSITY MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PRINCETON
|
||||||
|
UNIVERSITY MAKES NO REPRESENTATIONS OR WARRANTIES OF MERCHANT-
|
||||||
|
ABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE
|
||||||
|
LICENSED SOFTWARE, DATABASE OR DOCUMENTATION WILL NOT INFRINGE ANY
|
||||||
|
THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.
|
||||||
|
The name of Princeton University or Princeton may not be used in
|
||||||
|
advertising or publicity pertaining to distribution of the software
|
||||||
|
and/or database. Title to copyright in this software, database and
|
||||||
|
any associated documentation shall at all times remain with
|
||||||
|
Princeton University and LICENSEE agrees to preserve same.
|
||||||
|
The 40 level includes words from Alan's 3esl list found in version 4.0
|
||||||
|
of his 12dicts package. Like his other stuff the 3esl list is also in the
|
||||||
|
public domain.
|
||||||
|
The 50 level includes Brian's frequency class 1, words appearing
|
||||||
|
in at least 5 of 12 of the dictionaries as indicated in the 12Dicts
|
||||||
|
package, and uppercase words in at least 4 of the previous 12
|
||||||
|
dictionaries. A decent number of proper names is also included: The
|
||||||
|
top 1000 male, female, and Last names from the 1990 Census report; a
|
||||||
|
list of names sent to me by Alan Beale; and a few names that I added
|
||||||
|
myself. Finally a small list of abbreviations not commonly found in
|
||||||
|
other word lists is included.
|
||||||
|
The name files form the Census report is a government document which I
|
||||||
|
don't think can be copyrighted.
|
||||||
|
The file special-jargon.50 uses common.lst and word.lst from the
|
||||||
|
"Unofficial Jargon File Word Lists" which is derived from "The Jargon
|
||||||
|
File". All of which is in the Public Domain. This file also contain
|
||||||
|
a few extra UNIX terms which are found in the file "unix-terms" in the
|
||||||
|
special/ directory.
|
||||||
|
The 55 level includes words from Alan's 2of4brif list found in version
|
||||||
|
4.0 of his 12dicts package. Like his other stuff the 2of4brif is also
|
||||||
|
in the public domain.
|
||||||
|
The 60 level includes all words appearing in at least 2 of the 12
|
||||||
|
dictionaries as indicated by the 12Dicts package.
|
||||||
|
The 70 level includes Brian's frequency class 0 and the 74,550 common
|
||||||
|
dictionary words from the MWords package. The common dictionary words,
|
||||||
|
like those from the 12Dicts package, have had all likely inflections
|
||||||
|
added. The 70 level also included the 5desk list from version 4.0 of
|
||||||
|
the 12Dics package which is in the public domain.
|
||||||
|
The 80 level includes the ENABLE word list, all the lists in the
|
||||||
|
ENABLE supplement package (except for ABLE), the "UK Advanced Cryptics
|
||||||
|
Dictionary" (UKACD), the list of signature words from the YAWL package,
|
||||||
|
and the 10,196 places list from the MWords package.
|
||||||
|
The ENABLE package, mainted by M\Cooper <thegrendel@theriver.com>,
|
||||||
|
is in the Public Domain:
|
||||||
|
The ENABLE master word list, WORD.LST, is herewith formally released
|
||||||
|
into the Public Domain. Anyone is free to use it or distribute it in
|
||||||
|
any manner they see fit. No fee or registration is required for its
|
||||||
|
use nor are "contributions" solicited (if you feel you absolutely
|
||||||
|
must contribute something for your own peace of mind, the authors of
|
||||||
|
the ENABLE list ask that you make a donation on their behalf to your
|
||||||
|
favorite charity). This word list is our gift to the Scrabble
|
||||||
|
community, as an alternate to "official" word lists. Game designers
|
||||||
|
may feel free to incorporate the WORD.LST into their games. Please
|
||||||
|
mention the source and credit us as originators of the list. Note
|
||||||
|
that if you, as a game designer, use the WORD.LST in your product,
|
||||||
|
you may still copyright and protect your product, but you may *not*
|
||||||
|
legally copyright or in any way restrict redistribution of the
|
||||||
|
WORD.LST portion of your product. This *may* under law restrict your
|
||||||
|
rights to restrict your users' rights, but that is only fair.
|
||||||
|
UKACD, by J Ross Beresford <ross@bryson.demon.co.uk>, is under the
|
||||||
|
following copyright:
|
||||||
|
Copyright (c) J Ross Beresford 1993-1999. All Rights Reserved.
|
||||||
|
The following restriction is placed on the use of this publication:
|
||||||
|
if The UK Advanced Cryptics Dictionary is used in a software package
|
||||||
|
or redistributed in any form, the copyright notice must be
|
||||||
|
prominently displayed and the text of this document must be included
|
||||||
|
verbatim.
|
||||||
|
There are no other restrictions: I would like to see the list
|
||||||
|
distributed as widely as possible.
|
||||||
|
The 95 level includes the 354,984 single words, 256,772 compound
|
||||||
|
words, 4,946 female names and the 3,897 male names, and 21,986 names
|
||||||
|
from the MWords package, ABLE.LST from the ENABLE Supplement, and some
|
||||||
|
additional words found in my part-of-speech database that were not
|
||||||
|
found anywhere else.
|
||||||
|
Accent information was taken from UKACD.
|
||||||
|
The VarCon package was used to create the American, British, Canadian,
|
||||||
|
and Australian word list. It is under the following copyright:
|
||||||
|
Copyright 2000-2016 by Kevin Atkinson
|
||||||
|
Permission to use, copy, modify, distribute and sell this array, the
|
||||||
|
associated software, and its documentation for any purpose is hereby
|
||||||
|
granted without fee, provided that the above copyright notice appears
|
||||||
|
in all copies and that both that copyright notice and this permission
|
||||||
|
notice appear in supporting documentation. Kevin Atkinson makes no
|
||||||
|
representations about the suitability of this array for any
|
||||||
|
purpose. It is provided "as is" without express or implied warranty.
|
||||||
|
Copyright 2016 by Benjamin Titze
|
||||||
|
Permission to use, copy, modify, distribute and sell this array, the
|
||||||
|
associated software, and its documentation for any purpose is hereby
|
||||||
|
granted without fee, provided that the above copyright notice appears
|
||||||
|
in all copies and that both that copyright notice and this permission
|
||||||
|
notice appear in supporting documentation. Benjamin Titze makes no
|
||||||
|
representations about the suitability of this array for any
|
||||||
|
purpose. It is provided "as is" without express or implied warranty.
|
||||||
|
Since the original words lists come from the Ispell distribution:
|
||||||
|
Copyright 1993, Geoff Kuenning, Granada Hills, CA
|
||||||
|
All rights reserved.
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
3. All modifications to the source code must be clearly marked as
|
||||||
|
such. Binary redistributions based on modified source code
|
||||||
|
must be clearly marked as modified versions in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
(clause 4 removed with permission from Geoff Kuenning)
|
||||||
|
5. The name of Geoff Kuenning may not be used to endorse or promote
|
||||||
|
products derived from this software without specific prior
|
||||||
|
written permission.
|
||||||
|
THIS SOFTWARE IS PROVIDED BY GEOFF KUENNING AND CONTRIBUTORS ``AS IS'' AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL GEOFF KUENNING OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||||
|
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||||
|
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||||
|
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGE.
|
||||||
|
Build Date: Sun Jan 22 17:42:42 EST 2017
|
516
resources/hunspell_dictionaries/LICENSE-hunspell.txt
Normal file
516
resources/hunspell_dictionaries/LICENSE-hunspell.txt
Normal file
@ -0,0 +1,516 @@
|
|||||||
|
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 2.1, February 1999
|
||||||
|
|
||||||
|
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||||
|
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
[This is the first released version of the Lesser GPL. It also counts
|
||||||
|
as the successor of the GNU Library Public License, version 2, hence
|
||||||
|
the version number 2.1.]
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
Licenses are intended to guarantee your freedom to share and change
|
||||||
|
free software--to make sure the software is free for all its users.
|
||||||
|
|
||||||
|
This license, the Lesser General Public License, applies to some
|
||||||
|
specially designated software packages--typically libraries--of the
|
||||||
|
Free Software Foundation and other authors who decide to use it. You
|
||||||
|
can use it too, but we suggest you first think carefully about whether
|
||||||
|
this license or the ordinary General Public License is the better
|
||||||
|
strategy to use in any particular case, based on the explanations
|
||||||
|
below.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom of use,
|
||||||
|
not price. Our General Public Licenses are designed to make sure that
|
||||||
|
you have the freedom to distribute copies of free software (and charge
|
||||||
|
for this service if you wish); that you receive source code or can get
|
||||||
|
it if you want it; that you can change the software and use pieces of
|
||||||
|
it in new free programs; and that you are informed that you can do
|
||||||
|
these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
distributors to deny you these rights or to ask you to surrender these
|
||||||
|
rights. These restrictions translate to certain responsibilities for
|
||||||
|
you if you distribute copies of the library or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of the library, whether gratis
|
||||||
|
or for a fee, you must give the recipients all the rights that we gave
|
||||||
|
you. You must make sure that they, too, receive or can get the source
|
||||||
|
code. If you link other code with the library, you must provide
|
||||||
|
complete object files to the recipients, so that they can relink them
|
||||||
|
with the library after making changes to the library and recompiling
|
||||||
|
it. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with a two-step method: (1) we copyright the
|
||||||
|
library, and (2) we offer you this license, which gives you legal
|
||||||
|
permission to copy, distribute and/or modify the library.
|
||||||
|
|
||||||
|
To protect each distributor, we want to make it very clear that
|
||||||
|
there is no warranty for the free library. Also, if the library is
|
||||||
|
modified by someone else and passed on, the recipients should know
|
||||||
|
that what they have is not the original version, so that the original
|
||||||
|
author's reputation will not be affected by problems that might be
|
||||||
|
introduced by others.
|
||||||
|
^L
|
||||||
|
Finally, software patents pose a constant threat to the existence of
|
||||||
|
any free program. We wish to make sure that a company cannot
|
||||||
|
effectively restrict the users of a free program by obtaining a
|
||||||
|
restrictive license from a patent holder. Therefore, we insist that
|
||||||
|
any patent license obtained for a version of the library must be
|
||||||
|
consistent with the full freedom of use specified in this license.
|
||||||
|
|
||||||
|
Most GNU software, including some libraries, is covered by the
|
||||||
|
ordinary GNU General Public License. This license, the GNU Lesser
|
||||||
|
General Public License, applies to certain designated libraries, and
|
||||||
|
is quite different from the ordinary General Public License. We use
|
||||||
|
this license for certain libraries in order to permit linking those
|
||||||
|
libraries into non-free programs.
|
||||||
|
|
||||||
|
When a program is linked with a library, whether statically or using
|
||||||
|
a shared library, the combination of the two is legally speaking a
|
||||||
|
combined work, a derivative of the original library. The ordinary
|
||||||
|
General Public License therefore permits such linking only if the
|
||||||
|
entire combination fits its criteria of freedom. The Lesser General
|
||||||
|
Public License permits more lax criteria for linking other code with
|
||||||
|
the library.
|
||||||
|
|
||||||
|
We call this license the "Lesser" General Public License because it
|
||||||
|
does Less to protect the user's freedom than the ordinary General
|
||||||
|
Public License. It also provides other free software developers Less
|
||||||
|
of an advantage over competing non-free programs. These disadvantages
|
||||||
|
are the reason we use the ordinary General Public License for many
|
||||||
|
libraries. However, the Lesser license provides advantages in certain
|
||||||
|
special circumstances.
|
||||||
|
|
||||||
|
For example, on rare occasions, there may be a special need to
|
||||||
|
encourage the widest possible use of a certain library, so that it
|
||||||
|
becomes
|
||||||
|
a de-facto standard. To achieve this, non-free programs must be
|
||||||
|
allowed to use the library. A more frequent case is that a free
|
||||||
|
library does the same job as widely used non-free libraries. In this
|
||||||
|
case, there is little to gain by limiting the free library to free
|
||||||
|
software only, so we use the Lesser General Public License.
|
||||||
|
|
||||||
|
In other cases, permission to use a particular library in non-free
|
||||||
|
programs enables a greater number of people to use a large body of
|
||||||
|
free software. For example, permission to use the GNU C Library in
|
||||||
|
non-free programs enables many more people to use the whole GNU
|
||||||
|
operating system, as well as its variant, the GNU/Linux operating
|
||||||
|
system.
|
||||||
|
|
||||||
|
Although the Lesser General Public License is Less protective of the
|
||||||
|
users' freedom, it does ensure that the user of a program that is
|
||||||
|
linked with the Library has the freedom and the wherewithal to run
|
||||||
|
that program using a modified version of the Library.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow. Pay close attention to the difference between a
|
||||||
|
"work based on the library" and a "work that uses the library". The
|
||||||
|
former contains code derived from the library, whereas the latter must
|
||||||
|
be combined with the library in order to run.
|
||||||
|
^L
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License Agreement applies to any software library or other
|
||||||
|
program which contains a notice placed by the copyright holder or
|
||||||
|
other authorized party saying it may be distributed under the terms of
|
||||||
|
this Lesser General Public License (also called "this License").
|
||||||
|
Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
A "library" means a collection of software functions and/or data
|
||||||
|
prepared so as to be conveniently linked with application programs
|
||||||
|
(which use some of those functions and data) to form executables.
|
||||||
|
|
||||||
|
The "Library", below, refers to any such software library or work
|
||||||
|
which has been distributed under these terms. A "work based on the
|
||||||
|
Library" means either the Library or any derivative work under
|
||||||
|
copyright law: that is to say, a work containing the Library or a
|
||||||
|
portion of it, either verbatim or with modifications and/or translated
|
||||||
|
straightforwardly into another language. (Hereinafter, translation is
|
||||||
|
included without limitation in the term "modification".)
|
||||||
|
|
||||||
|
"Source code" for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For a library, complete source code means
|
||||||
|
all the source code for all modules it contains, plus any associated
|
||||||
|
interface definition files, plus the scripts used to control
|
||||||
|
compilation
|
||||||
|
and installation of the library.
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running a program using the Library is not restricted, and output from
|
||||||
|
such a program is covered only if its contents constitute a work based
|
||||||
|
on the Library (independent of the use of the Library in a tool for
|
||||||
|
writing it). Whether that is true depends on what the Library does
|
||||||
|
and what the program that uses the Library does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Library's
|
||||||
|
complete source code as you receive it, in any medium, provided that
|
||||||
|
you conspicuously and appropriately publish on each copy an
|
||||||
|
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||||
|
all the notices that refer to this License and to the absence of any
|
||||||
|
warranty; and distribute a copy of this License along with the
|
||||||
|
Library.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy,
|
||||||
|
and you may at your option offer warranty protection in exchange for a
|
||||||
|
fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Library or any portion
|
||||||
|
of it, thus forming a work based on the Library, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The modified work must itself be a software library.
|
||||||
|
|
||||||
|
b) You must cause the files modified to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
c) You must cause the whole of the work to be licensed at no
|
||||||
|
charge to all third parties under the terms of this License.
|
||||||
|
|
||||||
|
d) If a facility in the modified Library refers to a function or a
|
||||||
|
table of data to be supplied by an application program that uses
|
||||||
|
the facility, other than as an argument passed when the facility
|
||||||
|
is invoked, then you must make a good faith effort to ensure that,
|
||||||
|
in the event an application does not supply such function or
|
||||||
|
table, the facility still operates, and performs whatever part of
|
||||||
|
its purpose remains meaningful.
|
||||||
|
|
||||||
|
(For example, a function in a library to compute square roots has
|
||||||
|
a purpose that is entirely well-defined independent of the
|
||||||
|
application. Therefore, Subsection 2d requires that any
|
||||||
|
application-supplied function or table used by this function must
|
||||||
|
be optional: if the application does not supply it, the square
|
||||||
|
root function must still compute square roots.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Library,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Library, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote
|
||||||
|
it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Library.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Library
|
||||||
|
with the Library (or with a work based on the Library) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||||
|
License instead of this License to a given copy of the Library. To do
|
||||||
|
this, you must alter all the notices that refer to this License, so
|
||||||
|
that they refer to the ordinary GNU General Public License, version 2,
|
||||||
|
instead of to this License. (If a newer version than version 2 of the
|
||||||
|
ordinary GNU General Public License has appeared, then you can specify
|
||||||
|
that version instead if you wish.) Do not make any other change in
|
||||||
|
these notices.
|
||||||
|
^L
|
||||||
|
Once this change is made in a given copy, it is irreversible for
|
||||||
|
that copy, so the ordinary GNU General Public License applies to all
|
||||||
|
subsequent copies and derivative works made from that copy.
|
||||||
|
|
||||||
|
This option is useful when you wish to copy part of the code of
|
||||||
|
the Library into a program that is not a library.
|
||||||
|
|
||||||
|
4. You may copy and distribute the Library (or a portion or
|
||||||
|
derivative of it, under Section 2) in object code or executable form
|
||||||
|
under the terms of Sections 1 and 2 above provided that you accompany
|
||||||
|
it with the complete corresponding machine-readable source code, which
|
||||||
|
must be distributed under the terms of Sections 1 and 2 above on a
|
||||||
|
medium customarily used for software interchange.
|
||||||
|
|
||||||
|
If distribution of object code is made by offering access to copy
|
||||||
|
from a designated place, then offering equivalent access to copy the
|
||||||
|
source code from the same place satisfies the requirement to
|
||||||
|
distribute the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
5. A program that contains no derivative of any portion of the
|
||||||
|
Library, but is designed to work with the Library by being compiled or
|
||||||
|
linked with it, is called a "work that uses the Library". Such a
|
||||||
|
work, in isolation, is not a derivative work of the Library, and
|
||||||
|
therefore falls outside the scope of this License.
|
||||||
|
|
||||||
|
However, linking a "work that uses the Library" with the Library
|
||||||
|
creates an executable that is a derivative of the Library (because it
|
||||||
|
contains portions of the Library), rather than a "work that uses the
|
||||||
|
library". The executable is therefore covered by this License.
|
||||||
|
Section 6 states terms for distribution of such executables.
|
||||||
|
|
||||||
|
When a "work that uses the Library" uses material from a header file
|
||||||
|
that is part of the Library, the object code for the work may be a
|
||||||
|
derivative work of the Library even though the source code is not.
|
||||||
|
Whether this is true is especially significant if the work can be
|
||||||
|
linked without the Library, or if the work is itself a library. The
|
||||||
|
threshold for this to be true is not precisely defined by law.
|
||||||
|
|
||||||
|
If such an object file uses only numerical parameters, data
|
||||||
|
structure layouts and accessors, and small macros and small inline
|
||||||
|
functions (ten lines or less in length), then the use of the object
|
||||||
|
file is unrestricted, regardless of whether it is legally a derivative
|
||||||
|
work. (Executables containing this object code plus portions of the
|
||||||
|
Library will still fall under Section 6.)
|
||||||
|
|
||||||
|
Otherwise, if the work is a derivative of the Library, you may
|
||||||
|
distribute the object code for the work under the terms of Section 6.
|
||||||
|
Any executables containing that work also fall under Section 6,
|
||||||
|
whether or not they are linked directly with the Library itself.
|
||||||
|
^L
|
||||||
|
6. As an exception to the Sections above, you may also combine or
|
||||||
|
link a "work that uses the Library" with the Library to produce a
|
||||||
|
work containing portions of the Library, and distribute that work
|
||||||
|
under terms of your choice, provided that the terms permit
|
||||||
|
modification of the work for the customer's own use and reverse
|
||||||
|
engineering for debugging such modifications.
|
||||||
|
|
||||||
|
You must give prominent notice with each copy of the work that the
|
||||||
|
Library is used in it and that the Library and its use are covered by
|
||||||
|
this License. You must supply a copy of this License. If the work
|
||||||
|
during execution displays copyright notices, you must include the
|
||||||
|
copyright notice for the Library among them, as well as a reference
|
||||||
|
directing the user to the copy of this License. Also, you must do one
|
||||||
|
of these things:
|
||||||
|
|
||||||
|
a) Accompany the work with the complete corresponding
|
||||||
|
machine-readable source code for the Library including whatever
|
||||||
|
changes were used in the work (which must be distributed under
|
||||||
|
Sections 1 and 2 above); and, if the work is an executable linked
|
||||||
|
with the Library, with the complete machine-readable "work that
|
||||||
|
uses the Library", as object code and/or source code, so that the
|
||||||
|
user can modify the Library and then relink to produce a modified
|
||||||
|
executable containing the modified Library. (It is understood
|
||||||
|
that the user who changes the contents of definitions files in the
|
||||||
|
Library will not necessarily be able to recompile the application
|
||||||
|
to use the modified definitions.)
|
||||||
|
|
||||||
|
b) Use a suitable shared library mechanism for linking with the
|
||||||
|
Library. A suitable mechanism is one that (1) uses at run time a
|
||||||
|
copy of the library already present on the user's computer system,
|
||||||
|
rather than copying library functions into the executable, and (2)
|
||||||
|
will operate properly with a modified version of the library, if
|
||||||
|
the user installs one, as long as the modified version is
|
||||||
|
interface-compatible with the version that the work was made with.
|
||||||
|
|
||||||
|
c) Accompany the work with a written offer, valid for at
|
||||||
|
least three years, to give the same user the materials
|
||||||
|
specified in Subsection 6a, above, for a charge no more
|
||||||
|
than the cost of performing this distribution.
|
||||||
|
|
||||||
|
d) If distribution of the work is made by offering access to copy
|
||||||
|
from a designated place, offer equivalent access to copy the above
|
||||||
|
specified materials from the same place.
|
||||||
|
|
||||||
|
e) Verify that the user has already received a copy of these
|
||||||
|
materials or that you have already sent this user a copy.
|
||||||
|
|
||||||
|
For an executable, the required form of the "work that uses the
|
||||||
|
Library" must include any data and utility programs needed for
|
||||||
|
reproducing the executable from it. However, as a special exception,
|
||||||
|
the materials to be distributed need not include anything that is
|
||||||
|
normally distributed (in either source or binary form) with the major
|
||||||
|
components (compiler, kernel, and so on) of the operating system on
|
||||||
|
which the executable runs, unless that component itself accompanies
|
||||||
|
the executable.
|
||||||
|
|
||||||
|
It may happen that this requirement contradicts the license
|
||||||
|
restrictions of other proprietary libraries that do not normally
|
||||||
|
accompany the operating system. Such a contradiction means you cannot
|
||||||
|
use both them and the Library together in an executable that you
|
||||||
|
distribute.
|
||||||
|
^L
|
||||||
|
7. You may place library facilities that are a work based on the
|
||||||
|
Library side-by-side in a single library together with other library
|
||||||
|
facilities not covered by this License, and distribute such a combined
|
||||||
|
library, provided that the separate distribution of the work based on
|
||||||
|
the Library and of the other library facilities is otherwise
|
||||||
|
permitted, and provided that you do these two things:
|
||||||
|
|
||||||
|
a) Accompany the combined library with a copy of the same work
|
||||||
|
based on the Library, uncombined with any other library
|
||||||
|
facilities. This must be distributed under the terms of the
|
||||||
|
Sections above.
|
||||||
|
|
||||||
|
b) Give prominent notice with the combined library of the fact
|
||||||
|
that part of it is a work based on the Library, and explaining
|
||||||
|
where to find the accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
8. You may not copy, modify, sublicense, link with, or distribute
|
||||||
|
the Library except as expressly provided under this License. Any
|
||||||
|
attempt otherwise to copy, modify, sublicense, link with, or
|
||||||
|
distribute the Library is void, and will automatically terminate your
|
||||||
|
rights under this License. However, parties who have received copies,
|
||||||
|
or rights, from you under this License will not have their licenses
|
||||||
|
terminated so long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
9. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Library or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Library (or any work based on the
|
||||||
|
Library), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Library or works based on it.
|
||||||
|
|
||||||
|
10. Each time you redistribute the Library (or any work based on the
|
||||||
|
Library), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute, link with or modify the Library
|
||||||
|
subject to these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties with
|
||||||
|
this License.
|
||||||
|
^L
|
||||||
|
11. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Library at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Library by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Library.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under
|
||||||
|
any particular circumstance, the balance of the section is intended to
|
||||||
|
apply, and the section as a whole is intended to apply in other
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
12. If the distribution and/or use of the Library is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Library under this License
|
||||||
|
may add an explicit geographical distribution limitation excluding those
|
||||||
|
countries, so that distribution is permitted only in or among
|
||||||
|
countries not thus excluded. In such case, this License incorporates
|
||||||
|
the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
13. The Free Software Foundation may publish revised and/or new
|
||||||
|
versions of the Lesser General Public License from time to time.
|
||||||
|
Such new versions will be similar in spirit to the present version,
|
||||||
|
but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Library
|
||||||
|
specifies a version number of this License which applies to it and
|
||||||
|
"any later version", you have the option of following the terms and
|
||||||
|
conditions either of that version or of any later version published by
|
||||||
|
the Free Software Foundation. If the Library does not specify a
|
||||||
|
license version number, you may choose any version ever published by
|
||||||
|
the Free Software Foundation.
|
||||||
|
^L
|
||||||
|
14. If you wish to incorporate parts of the Library into other free
|
||||||
|
programs whose distribution conditions are incompatible with these,
|
||||||
|
write to the author to ask for permission. For software which is
|
||||||
|
copyrighted by the Free Software Foundation, write to the Free
|
||||||
|
Software Foundation; we sometimes make exceptions for this. Our
|
||||||
|
decision will be guided by the two goals of preserving the free status
|
||||||
|
of all derivatives of our free software and of promoting the sharing
|
||||||
|
and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||||
|
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||||
|
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||||
|
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||||
|
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||||
|
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||||
|
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||||
|
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||||
|
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||||
|
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||||
|
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||||
|
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||||
|
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||||
|
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||||
|
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
|
DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
^L
|
||||||
|
How to Apply These Terms to Your New Libraries
|
||||||
|
|
||||||
|
If you develop a new library, and you want it to be of the greatest
|
||||||
|
possible use to the public, we recommend making it free software that
|
||||||
|
everyone can redistribute and change. You can do so by permitting
|
||||||
|
redistribution under these terms (or, alternatively, under the terms
|
||||||
|
of the ordinary General Public License).
|
||||||
|
|
||||||
|
To apply these terms, attach the following notices to the library.
|
||||||
|
It is safest to attach them to the start of each source file to most
|
||||||
|
effectively convey the exclusion of warranty; and each file should
|
||||||
|
have at least the "copyright" line and a pointer to where the full
|
||||||
|
notice is found.
|
||||||
|
|
||||||
|
|
||||||
|
<one line to give the library's name and a brief idea of what it
|
||||||
|
does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper
|
||||||
|
mail.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or
|
||||||
|
your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||||
|
library `Frob' (a library for tweaking knobs) written by James
|
||||||
|
Random Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1990
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
That's all there is to it!
|
||||||
|
|
||||||
|
|
||||||
|
|
BIN
resources/hunspell_dictionaries/en-US.bdic
Normal file
BIN
resources/hunspell_dictionaries/en-US.bdic
Normal file
Binary file not shown.
@ -68,7 +68,7 @@
|
|||||||
</script>
|
</script>
|
||||||
<% } %>
|
<% } %>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
||||||
<!-- Vue app -->
|
<!-- Vue app -->
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import parseArgs from '../cli/parser'
|
|||||||
import { normalizeMarkdownPath } from '../filesystem/markdown'
|
import { normalizeMarkdownPath } from '../filesystem/markdown'
|
||||||
import { selectTheme } from '../menu/actions/theme'
|
import { selectTheme } from '../menu/actions/theme'
|
||||||
import { dockMenu } from '../menu/templates'
|
import { dockMenu } from '../menu/templates'
|
||||||
|
import ensureDefaultDict from '../preferences/hunspell'
|
||||||
import { watchers } from '../utils/imagePathAutoComplement'
|
import { watchers } from '../utils/imagePathAutoComplement'
|
||||||
import { WindowType } from '../windows/base'
|
import { WindowType } from '../windows/base'
|
||||||
import EditorWindow from '../windows/editor'
|
import EditorWindow from '../windows/editor'
|
||||||
@ -109,6 +110,13 @@ class App {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Copy default (en-US) Hunspell dictionary.
|
||||||
|
const { paths } = this._accessor
|
||||||
|
ensureDefaultDict(paths.userDataPath)
|
||||||
|
.catch(error => {
|
||||||
|
log.error(error)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getScreenshotFileName () {
|
async getScreenshotFileName () {
|
||||||
|
28
src/main/preferences/hunspell.js
Normal file
28
src/main/preferences/hunspell.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
import { isOsx } from '../config'
|
||||||
|
|
||||||
|
// This is an asynchronous function to not block the process. The spell checker may be
|
||||||
|
// diabled on first application start because the dictionary doesn't exists or is incomplete.
|
||||||
|
export default async appDataPath => {
|
||||||
|
let srcPath = process.resourcesPath
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
// Default locations:
|
||||||
|
// Linux/Windows: node_modules/electron/dist/resources/
|
||||||
|
// macOS: node_modules/electron/dist/Electron.app/Contents/Resources
|
||||||
|
if (isOsx) {
|
||||||
|
srcPath = path.join(srcPath, '../..')
|
||||||
|
}
|
||||||
|
srcPath = path.join(srcPath, '../../../../resources')
|
||||||
|
}
|
||||||
|
srcPath = path.join(srcPath, 'hunspell_dictionaries/en-US.bdic')
|
||||||
|
|
||||||
|
// NOTE: Hardcoded in "@hfelix/electron-spellchecker/src/spell-check-handler.js"
|
||||||
|
const destDir = path.join(appDataPath, 'dictionaries')
|
||||||
|
const destPath = path.join(destDir, 'en-US.bdic')
|
||||||
|
|
||||||
|
if (!await fs.exists(destPath) && await fs.exists(srcPath)) {
|
||||||
|
await fs.ensureDir(destDir)
|
||||||
|
await fs.copy(srcPath, destPath)
|
||||||
|
}
|
||||||
|
}
|
@ -233,6 +233,32 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"spellcheckerEnabled": {
|
||||||
|
"description": "Spelling--Whether spell checking is enabled.",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"spellcheckerIsHunspell": {
|
||||||
|
"description": "Spelling--Whether Hunspell or the OS spell checker is used (macOS only).",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"spellcheckerNoUnderline": {
|
||||||
|
"description": "Spelling--Don't underline spelling mistakes.",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"spellcheckerAutoDetectLanguage": {
|
||||||
|
"description": "Spelling--Try to automatically identify the used language when typing.",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"spellcheckerLanguage": {
|
||||||
|
"description": "Spelling--The spell checker language",
|
||||||
|
"pattern": "^[a-z]{2}(?:[-][A-Z]{2})?$",
|
||||||
|
"default": "en-US"
|
||||||
|
},
|
||||||
|
|
||||||
"imageInsertAction": {
|
"imageInsertAction": {
|
||||||
"description": "Image--The default behavior after insert image from local folder",
|
"description": "Image--The default behavior after insert image from local folder",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
@ -400,6 +400,16 @@ class EditorWindow extends BaseWindow {
|
|||||||
|
|
||||||
// --- private ---------------------------------
|
// --- private ---------------------------------
|
||||||
|
|
||||||
|
_buildUrlString (windowId, env, userPreference) {
|
||||||
|
const url = this._buildUrlWithSettings(windowId, env, userPreference)
|
||||||
|
const spellcheckerIsHunspell = userPreference.getItem('spellcheckerIsHunspell')
|
||||||
|
|
||||||
|
// Add additional settings
|
||||||
|
url.searchParams.set('slp', spellcheckerIsHunspell ? '1' : '0')
|
||||||
|
|
||||||
|
return url.toString()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a new new tab from the markdown document.
|
* Open a new new tab from the markdown document.
|
||||||
*
|
*
|
||||||
|
@ -255,6 +255,10 @@ export const MUYA_DEFAULT_OPTION = {
|
|||||||
mermaidTheme: 'default', // dark / forest / default
|
mermaidTheme: 'default', // dark / forest / default
|
||||||
vegaTheme: 'latimes', // excel / ggplot2 / quartz / vox / fivethirtyeight / dark / latimes
|
vegaTheme: 'latimes', // excel / ggplot2 / quartz / vox / fivethirtyeight / dark / latimes
|
||||||
hideQuickInsertHint: false,
|
hideQuickInsertHint: false,
|
||||||
|
// Whether we should set spellcheck attribute on our container to highlight misspelled words.
|
||||||
|
// NOTE: The browser is not able to correct misspelled words words without a custom
|
||||||
|
// implementation like in Mark Text.
|
||||||
|
spellcheckEnabled: false,
|
||||||
// transform the image to local folder, cloud or just return the local path
|
// transform the image to local folder, cloud or just return the local path
|
||||||
imageAction: null,
|
imageAction: null,
|
||||||
// Call Electron open dialog or input element type is file.
|
// Call Electron open dialog or input element type is file.
|
||||||
|
56
src/muya/lib/contentState/core.js
Normal file
56
src/muya/lib/contentState/core.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
const coreApi = ContentState => {
|
||||||
|
/**
|
||||||
|
* Replace the word range with the given replacement.
|
||||||
|
*
|
||||||
|
* @param {*} line A line block reference of the line that contains the word to
|
||||||
|
* replace - must be a valid reference!
|
||||||
|
* @param {*} wordCursor The range of the word to replace (line: "abc >foo< abc"
|
||||||
|
* whereas `>`/`<` is start and end of `wordCursor`). This
|
||||||
|
* range is replaced by `replacement`.
|
||||||
|
* @param {string} replacement The replacement.
|
||||||
|
* @param {boolean} setCursor Shoud we update the editor cursor?
|
||||||
|
*/
|
||||||
|
ContentState.prototype.replaceWordInline = function (line, wordCursor, replacement, setCursor = false) {
|
||||||
|
const { start: lineStart, end: lineEnd } = line
|
||||||
|
const { start: wordStart, end: wordEnd } = wordCursor
|
||||||
|
|
||||||
|
// Validate cursor ranges.
|
||||||
|
if (wordStart.key !== wordEnd.key) {
|
||||||
|
throw new Error('Expect a single line word cursor: "start.key" is not equal to "end.key".')
|
||||||
|
} else if (lineStart.key !== lineEnd.key) {
|
||||||
|
throw new Error('Expect a single line line cursor: "start.key" is not equal to "end.key".')
|
||||||
|
} else if (wordStart.offset > wordEnd.offset) {
|
||||||
|
throw new Error(`Invalid word cursor offset: ${wordStart.offset} should be less ${wordEnd.offset}.`)
|
||||||
|
} else if (lineStart.key !== wordEnd.key) {
|
||||||
|
throw new Error(`Cursor mismatch: Expect the same line but got ${lineStart.key} and ${wordEnd.key}.`)
|
||||||
|
} else if (lineStart.block.text.length < wordEnd.offset) {
|
||||||
|
throw new Error('Invalid cursor: Replacement length is larger than line length.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { block } = lineStart
|
||||||
|
const { offset: left } = wordStart
|
||||||
|
const { offset: right } = wordEnd
|
||||||
|
|
||||||
|
// Replace word range with replacement.
|
||||||
|
block.text = block.text.substr(0, left) + replacement + block.text.substr(right)
|
||||||
|
|
||||||
|
// Update cursor
|
||||||
|
if (setCursor) {
|
||||||
|
const cursor = Object.assign({}, wordStart, {
|
||||||
|
offset: left + replacement.length
|
||||||
|
})
|
||||||
|
line.start = cursor
|
||||||
|
line.end = cursor
|
||||||
|
this.cursor = {
|
||||||
|
start: cursor,
|
||||||
|
end: cursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.partialRender()
|
||||||
|
this.muya.dispatchSelectionChange()
|
||||||
|
this.muya.dispatchChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default coreApi
|
@ -10,6 +10,7 @@ import codeBlockCtrl from './codeBlockCtrl'
|
|||||||
import tableBlockCtrl from './tableBlockCtrl'
|
import tableBlockCtrl from './tableBlockCtrl'
|
||||||
import tableDragBarCtrl from './tableDragBarCtrl'
|
import tableDragBarCtrl from './tableDragBarCtrl'
|
||||||
import tableSelectCellsCtrl from './tableSelectCellsCtrl'
|
import tableSelectCellsCtrl from './tableSelectCellsCtrl'
|
||||||
|
import coreApi from './core'
|
||||||
import History from './history'
|
import History from './history'
|
||||||
import arrowCtrl from './arrowCtrl'
|
import arrowCtrl from './arrowCtrl'
|
||||||
import pasteCtrl from './pasteCtrl'
|
import pasteCtrl from './pasteCtrl'
|
||||||
@ -32,6 +33,7 @@ import Cursor from '../selection/cursor'
|
|||||||
import escapeCharactersMap, { escapeCharacters } from '../parser/escapeCharacter'
|
import escapeCharactersMap, { escapeCharacters } from '../parser/escapeCharacter'
|
||||||
|
|
||||||
const prototypes = [
|
const prototypes = [
|
||||||
|
coreApi,
|
||||||
tabCtrl,
|
tabCtrl,
|
||||||
enterCtrl,
|
enterCtrl,
|
||||||
updateCtrl,
|
updateCtrl,
|
||||||
|
@ -28,9 +28,9 @@ class ClickEvent {
|
|||||||
if (!start || !end) {
|
if (!start || !end) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const startBlock = contentState.getBlock(start.key)
|
const startBlock = contentState.getBlock(start.key)
|
||||||
const nextTextBlock = contentState.findNextBlockInLocation(startBlock)
|
const nextTextBlock = contentState.findNextBlockInLocation(startBlock)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
nextTextBlock && nextTextBlock.key === end.key &&
|
nextTextBlock && nextTextBlock.key === end.key &&
|
||||||
end.offset === 0 &&
|
end.offset === 0 &&
|
||||||
|
@ -371,6 +371,12 @@ class Muya {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set spellcheck container attribute
|
||||||
|
const spellcheckEnabled = options.spellcheckEnabled
|
||||||
|
if (typeof spellcheckEnabled !== 'undefined') {
|
||||||
|
this.container.setAttribute('spellcheck', !!spellcheckEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
if (options.bulletListMarker) {
|
if (options.bulletListMarker) {
|
||||||
this.contentState.turndownConfig.bulletListMarker = options.bulletListMarker
|
this.contentState.turndownConfig.bulletListMarker = options.bulletListMarker
|
||||||
}
|
}
|
||||||
@ -380,6 +386,21 @@ class Muya {
|
|||||||
return this.keyboard.hideAllFloatTools()
|
return this.keyboard.hideAllFloatTools()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the word range with the given replacement.
|
||||||
|
*
|
||||||
|
* @param {*} line A line block reference of the line that contains the word to
|
||||||
|
* replace - must be a valid reference!
|
||||||
|
* @param {*} wordCursor The range of the word to replace (line: "abc >foo< abc"
|
||||||
|
* whereas `>`/`<` is start and end of `wordCursor`). This
|
||||||
|
* range is replaced by `replacement`.
|
||||||
|
* @param {string} replacement The replacement.
|
||||||
|
* @param {boolean} setCursor Shoud we update the editor cursor?
|
||||||
|
*/
|
||||||
|
replaceWordInline (line, wordCursor, replacement, setCursor = false) {
|
||||||
|
this.contentState.replaceWordInline(line, wordCursor, replacement, setCursor)
|
||||||
|
}
|
||||||
|
|
||||||
destroy () {
|
destroy () {
|
||||||
this.contentState.clear()
|
this.contentState.clear()
|
||||||
this.quickInsert.destroy()
|
this.quickInsert.destroy()
|
||||||
@ -395,7 +416,7 @@ class Muya {
|
|||||||
* [ensureContainerDiv ensure container element is div]
|
* [ensureContainerDiv ensure container element is div]
|
||||||
*/
|
*/
|
||||||
function getContainer (originContainer, options) {
|
function getContainer (originContainer, options) {
|
||||||
const { hideQuickInsertHint } = options
|
const { hideQuickInsertHint, spellcheckEnabled } = options
|
||||||
const container = document.createElement('div')
|
const container = document.createElement('div')
|
||||||
const rootDom = document.createElement('div')
|
const rootDom = document.createElement('div')
|
||||||
const attrs = originContainer.attributes
|
const attrs = originContainer.attributes
|
||||||
@ -411,7 +432,9 @@ function getContainer (originContainer, options) {
|
|||||||
container.setAttribute('contenteditable', true)
|
container.setAttribute('contenteditable', true)
|
||||||
container.setAttribute('autocorrect', false)
|
container.setAttribute('autocorrect', false)
|
||||||
container.setAttribute('autocomplete', 'off')
|
container.setAttribute('autocomplete', 'off')
|
||||||
container.setAttribute('spellcheck', false)
|
// NOTE: The browser is not able to correct misspelled words words without
|
||||||
|
// a custom implementation like in Mark Text.
|
||||||
|
container.setAttribute('spellcheck', !!spellcheckEnabled)
|
||||||
container.appendChild(rootDom)
|
container.appendChild(rootDom)
|
||||||
originContainer.replaceWith(container)
|
originContainer.replaceWith(container)
|
||||||
return container
|
return container
|
||||||
|
@ -39,11 +39,15 @@ export default function renderContainerBlock (parent, block, activeBlocks, match
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (editable === false) {
|
if (editable === false) {
|
||||||
Object.assign(data.attrs, { contenteditable: 'false' })
|
Object.assign(data.attrs, {
|
||||||
|
contenteditable: 'false',
|
||||||
|
spellcheck: 'false'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/code|pre/.test(type) && typeof lang === 'string' && !!lang) {
|
if (/code|pre/.test(type) && typeof lang === 'string' && !!lang) {
|
||||||
selector += `.language-${lang.replace(/[#.]{1}/g, '')}`
|
selector += `.language-${lang.replace(/[#.]{1}/g, '')}`
|
||||||
|
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/th|td/.test(type)) {
|
if (/th|td/.test(type)) {
|
||||||
@ -131,6 +135,7 @@ export default function renderContainerBlock (parent, block, activeBlocks, match
|
|||||||
/html|multiplemath|flowchart|mermaid|sequence|vega-lite/.test(functionType)
|
/html|multiplemath|flowchart|mermaid|sequence|vega-lite/.test(functionType)
|
||||||
) {
|
) {
|
||||||
selector += `.${CLASS_OR_ID.AG_CONTAINER_BLOCK}`
|
selector += `.${CLASS_OR_ID.AG_CONTAINER_BLOCK}`
|
||||||
|
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||||
}
|
}
|
||||||
} else if (/ul|ol/.test(type) && listType) {
|
} else if (/ul|ol/.test(type) && listType) {
|
||||||
selector += `.ag-${listType}-list`
|
selector += `.ag-${listType}-list`
|
||||||
@ -143,6 +148,7 @@ export default function renderContainerBlock (parent, block, activeBlocks, match
|
|||||||
selector += `.ag-${listItemType}-list-item`
|
selector += `.ag-${listItemType}-list-item`
|
||||||
selector += isLooseListItem ? `.${CLASS_OR_ID.AG_LOOSE_LIST_ITEM}` : `.${CLASS_OR_ID.AG_TIGHT_LIST_ITEM}`
|
selector += isLooseListItem ? `.${CLASS_OR_ID.AG_LOOSE_LIST_ITEM}` : `.${CLASS_OR_ID.AG_TIGHT_LIST_ITEM}`
|
||||||
} else if (type === 'pre') {
|
} else if (type === 'pre') {
|
||||||
|
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||||
Object.assign(data.dataset, { role: functionType })
|
Object.assign(data.dataset, { role: functionType })
|
||||||
selector += PRE_BLOCK_HASH[block.functionType]
|
selector += PRE_BLOCK_HASH[block.functionType]
|
||||||
|
|
||||||
|
@ -115,6 +115,7 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
|
|||||||
|
|
||||||
if (editable === false) {
|
if (editable === false) {
|
||||||
Object.assign(data.attrs, {
|
Object.assign(data.attrs, {
|
||||||
|
spellcheck: 'false',
|
||||||
contenteditable: 'false'
|
contenteditable: 'false'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -124,7 +125,10 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
|
|||||||
switch (functionType) {
|
switch (functionType) {
|
||||||
case 'html': {
|
case 'html': {
|
||||||
selector += `.${CLASS_OR_ID.AG_HTML_PREVIEW}`
|
selector += `.${CLASS_OR_ID.AG_HTML_PREVIEW}`
|
||||||
|
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||||
|
|
||||||
const htmlContent = sanitize(code, PREVIEW_DOMPURIFY_CONFIG)
|
const htmlContent = sanitize(code, PREVIEW_DOMPURIFY_CONFIG)
|
||||||
|
|
||||||
// handle empty html bock
|
// handle empty html bock
|
||||||
if (/^<([a-z][a-z\d]*)[^>]*?>(\s*)<\/\1>$/.test(htmlContent.trim())) {
|
if (/^<([a-z][a-z\d]*)[^>]*?>(\s*)<\/\1>$/.test(htmlContent.trim())) {
|
||||||
children = htmlToVNode('<div class="ag-empty"><Empty HTML Block></div>')
|
children = htmlToVNode('<div class="ag-empty"><Empty HTML Block></div>')
|
||||||
@ -145,6 +149,7 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
|
|||||||
case 'multiplemath': {
|
case 'multiplemath': {
|
||||||
const key = `${code}_display_math`
|
const key = `${code}_display_math`
|
||||||
selector += `.${CLASS_OR_ID.AG_CONTAINER_PREVIEW}`
|
selector += `.${CLASS_OR_ID.AG_CONTAINER_PREVIEW}`
|
||||||
|
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||||
if (code === '') {
|
if (code === '') {
|
||||||
children = '< Empty Mathematical Formula >'
|
children = '< Empty Mathematical Formula >'
|
||||||
selector += `.${CLASS_OR_ID.AG_EMPTY}`
|
selector += `.${CLASS_OR_ID.AG_EMPTY}`
|
||||||
@ -167,6 +172,7 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
|
|||||||
}
|
}
|
||||||
case 'mermaid': {
|
case 'mermaid': {
|
||||||
selector += `.${CLASS_OR_ID.AG_CONTAINER_PREVIEW}`
|
selector += `.${CLASS_OR_ID.AG_CONTAINER_PREVIEW}`
|
||||||
|
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||||
if (code === '') {
|
if (code === '') {
|
||||||
children = '< Empty Mermaid Block >'
|
children = '< Empty Mermaid Block >'
|
||||||
selector += `.${CLASS_OR_ID.AG_EMPTY}`
|
selector += `.${CLASS_OR_ID.AG_EMPTY}`
|
||||||
@ -186,6 +192,7 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
|
|||||||
case 'sequence':
|
case 'sequence':
|
||||||
case 'vega-lite': {
|
case 'vega-lite': {
|
||||||
selector += `.${CLASS_OR_ID.AG_CONTAINER_PREVIEW}`
|
selector += `.${CLASS_OR_ID.AG_CONTAINER_PREVIEW}`
|
||||||
|
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||||
if (code === '') {
|
if (code === '') {
|
||||||
children = '< Empty Diagram Block >'
|
children = '< Empty Diagram Block >'
|
||||||
selector += `.${CLASS_OR_ID.AG_EMPTY}`
|
selector += `.${CLASS_OR_ID.AG_EMPTY}`
|
||||||
@ -236,6 +243,7 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
|
|||||||
const html = getHighlightHtml(text, highlights)
|
const html = getHighlightHtml(text, highlights)
|
||||||
children = htmlToVNode(html)
|
children = htmlToVNode(html)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!block.parent) {
|
if (!block.parent) {
|
||||||
return h(selector, data, [this.renderIcon(block), ...children])
|
return h(selector, data, [this.renderIcon(block), ...children])
|
||||||
} else {
|
} else {
|
||||||
|
@ -13,6 +13,9 @@ export default function autoLink (h, cursor, block, token, outerClass) {
|
|||||||
return [
|
return [
|
||||||
h(`span.${className}`, startMarker),
|
h(`span.${className}`, startMarker),
|
||||||
h(`a.${CLASS_OR_ID.AG_INLINE_RULE}.${CLASS_OR_ID.AG_AUTO_LINK}`, {
|
h(`a.${CLASS_OR_ID.AG_INLINE_RULE}.${CLASS_OR_ID.AG_AUTO_LINK}`, {
|
||||||
|
attrs: {
|
||||||
|
spellcheck: 'false'
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
href: isLink ? encodeURI(href) : `mailto:${email}`,
|
href: isLink ? encodeURI(href) : `mailto:${email}`,
|
||||||
target: '_blank'
|
target: '_blank'
|
||||||
|
@ -9,6 +9,9 @@ export default function autoLinkExtension (h, cursor, block, token, outerClass)
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
h(`a.${CLASS_OR_ID.AG_INLINE_RULE}.${CLASS_OR_ID.AG_AUTO_LINK_EXTENSION}`, {
|
h(`a.${CLASS_OR_ID.AG_INLINE_RULE}.${CLASS_OR_ID.AG_AUTO_LINK_EXTENSION}`, {
|
||||||
|
attrs: {
|
||||||
|
spellcheck: 'false'
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
href: linkType === 'www' ? encodeURI(`http://${www}`) : (linkType === 'url' ? encodeURI(url) : `mailto:${email}`),
|
href: linkType === 'www' ? encodeURI(`http://${www}`) : (linkType === 'url' ? encodeURI(url) : `mailto:${email}`),
|
||||||
target: '_blank'
|
target: '_blank'
|
||||||
|
@ -9,6 +9,10 @@ export default function codeFense (h, cursor, block, token, outerClass) {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
h(`span.${CLASS_OR_ID.AG_GRAY}`, markerContent),
|
h(`span.${CLASS_OR_ID.AG_GRAY}`, markerContent),
|
||||||
h(`span.${CLASS_OR_ID.AG_LANGUAGE}`, content)
|
h(`span.${CLASS_OR_ID.AG_LANGUAGE}`, {
|
||||||
|
attrs: {
|
||||||
|
spellcheck: 'false'
|
||||||
|
}
|
||||||
|
}, content)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,9 @@ export default function emoji (h, cursor, block, token, outerClass) {
|
|||||||
|
|
||||||
const emojiVdom = validation
|
const emojiVdom = validation
|
||||||
? h(contentSelector, {
|
? h(contentSelector, {
|
||||||
|
attrs: {
|
||||||
|
spellcheck: 'false'
|
||||||
|
},
|
||||||
dataset: {
|
dataset: {
|
||||||
emoji: validation.emoji
|
emoji: validation.emoji
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,10 @@ export default function htmlRuby (h, cursor, block, token, outerClass) {
|
|||||||
h(`span.${className}.${CLASS_OR_ID.AG_RUBY}`, [
|
h(`span.${className}.${CLASS_OR_ID.AG_RUBY}`, [
|
||||||
h(`span.${CLASS_OR_ID.AG_INLINE_RULE}.${CLASS_OR_ID.AG_RUBY_TEXT}`, content),
|
h(`span.${CLASS_OR_ID.AG_INLINE_RULE}.${CLASS_OR_ID.AG_RUBY_TEXT}`, content),
|
||||||
h(previewSelector, {
|
h(previewSelector, {
|
||||||
attrs: { contenteditable: 'false' }
|
attrs: {
|
||||||
|
contenteditable: 'false',
|
||||||
|
spellcheck: 'false'
|
||||||
|
}
|
||||||
}, vNode)
|
}, vNode)
|
||||||
])
|
])
|
||||||
// if children is empty string, no need to render ruby charactors...
|
// if children is empty string, no need to render ruby charactors...
|
||||||
|
@ -20,6 +20,7 @@ export default function htmlTag (h, cursor, block, token, outerClass) {
|
|||||||
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
||||||
}, [])
|
}, [])
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
// Handle html img.
|
// Handle html img.
|
||||||
case 'img': {
|
case 'img': {
|
||||||
@ -49,6 +50,12 @@ export default function htmlTag (h, cursor, block, token, outerClass) {
|
|||||||
raw: token.raw
|
raw: token.raw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable spell checking for these tags
|
||||||
|
if (tag === 'code' || tag === 'kbd') {
|
||||||
|
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||||
|
}
|
||||||
|
|
||||||
if (attrs.id) {
|
if (attrs.id) {
|
||||||
selector += `#${attrs.id}`
|
selector += `#${attrs.id}`
|
||||||
}
|
}
|
||||||
@ -66,9 +73,17 @@ export default function htmlTag (h, cursor, block, token, outerClass) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
h(`span.${tagClassName}.${CLASS_OR_ID.AG_OUTPUT_REMOVE}`, openContent),
|
h(`span.${tagClassName}.${CLASS_OR_ID.AG_OUTPUT_REMOVE}`, {
|
||||||
|
attrs: {
|
||||||
|
spellcheck: 'false'
|
||||||
|
}
|
||||||
|
}, openContent),
|
||||||
h(`${selector}`, data, anchor),
|
h(`${selector}`, data, anchor),
|
||||||
h(`span.${tagClassName}.${CLASS_OR_ID.AG_OUTPUT_REMOVE}`, closeContent)
|
h(`span.${tagClassName}.${CLASS_OR_ID.AG_OUTPUT_REMOVE}`, {
|
||||||
|
attrs: {
|
||||||
|
spellcheck: 'false'
|
||||||
|
}
|
||||||
|
}, closeContent)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,11 @@ export default function inlineCode (h, cursor, block, token, outerClass) {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
h(`span.${className}.${CLASS_OR_ID.AG_REMOVE}`, startMarker),
|
h(`span.${className}.${CLASS_OR_ID.AG_REMOVE}`, startMarker),
|
||||||
h(`code.${CLASS_OR_ID.AG_INLINE_RULE}`, content),
|
h(`code.${CLASS_OR_ID.AG_INLINE_RULE}`, {
|
||||||
|
attrs: {
|
||||||
|
spellcheck: 'false'
|
||||||
|
}
|
||||||
|
}, content),
|
||||||
h(`span.${className}.${CLASS_OR_ID.AG_REMOVE}`, endMarker)
|
h(`span.${className}.${CLASS_OR_ID.AG_REMOVE}`, endMarker)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,9 @@ export default function displayMath (h, cursor, block, token, outerClass) {
|
|||||||
return [
|
return [
|
||||||
h(`span.${className}.${CLASS_OR_ID.AG_MATH_MARKER}`, startMarker),
|
h(`span.${className}.${CLASS_OR_ID.AG_MATH_MARKER}`, startMarker),
|
||||||
h(mathSelector, [
|
h(mathSelector, [
|
||||||
h(`span.${CLASS_OR_ID.AG_INLINE_RULE}.${CLASS_OR_ID.AG_MATH_TEXT}`, content),
|
h(`span.${CLASS_OR_ID.AG_INLINE_RULE}.${CLASS_OR_ID.AG_MATH_TEXT}`, {
|
||||||
|
attrs: { spellcheck: 'false' }
|
||||||
|
}, content),
|
||||||
h(previewSelector, {
|
h(previewSelector, {
|
||||||
attrs: { contenteditable: 'false' }
|
attrs: { contenteditable: 'false' }
|
||||||
}, mathVnode)
|
}, mathVnode)
|
||||||
|
@ -70,7 +70,9 @@ export default function link (h, cursor, block, token, outerClass) {
|
|||||||
...this.backlashInToken(h, token.backlash.first, className, firstBacklashStart, token)
|
...this.backlashInToken(h, token.backlash.first, className, firstBacklashStart, token)
|
||||||
]),
|
]),
|
||||||
h(`span.${className}.${CLASS_OR_ID.AG_REMOVE}`, middleBracket),
|
h(`span.${className}.${CLASS_OR_ID.AG_REMOVE}`, middleBracket),
|
||||||
h(`span.${linkClassName}.${CLASS_OR_ID.AG_REMOVE}`, [
|
h(`span.${linkClassName}.${CLASS_OR_ID.AG_REMOVE}`, {
|
||||||
|
attrs: { spellcheck: 'false' }
|
||||||
|
}, [
|
||||||
...hrefContent,
|
...hrefContent,
|
||||||
...this.backlashInToken(h, token.backlash.second, className, secondBacklashStart, token)
|
...this.backlashInToken(h, token.backlash.second, className, secondBacklashStart, token)
|
||||||
]),
|
]),
|
||||||
|
@ -42,10 +42,22 @@ export default function referenceDefinition (h, cursor, block, token, outerClass
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
h(`span.${className}`, leftBracketContent),
|
h(`span.${className}`, leftBracketContent),
|
||||||
h(`span.${CLASS_OR_ID.AG_REFERENCE_LABEL}`, labelContent),
|
h(`span.${CLASS_OR_ID.AG_REFERENCE_LABEL}`, {
|
||||||
|
attrs: {
|
||||||
|
spellcheck: 'false'
|
||||||
|
}
|
||||||
|
}, labelContent),
|
||||||
...this.backlashInToken(h, backlash, CLASS_OR_ID.AG_GRAY, backlashStart, token),
|
...this.backlashInToken(h, backlash, CLASS_OR_ID.AG_GRAY, backlashStart, token),
|
||||||
h(`span.${className}`, middleContent),
|
h(`span.${className}`, {
|
||||||
|
attrs: {
|
||||||
|
spellcheck: 'false'
|
||||||
|
}
|
||||||
|
}, middleContent),
|
||||||
h(`span.${CLASS_OR_ID.AG_REFERENCE_TITLE}`, titleContent),
|
h(`span.${CLASS_OR_ID.AG_REFERENCE_TITLE}`, titleContent),
|
||||||
h(`span.${className}`, rightContent)
|
h(`span.${className}`, {
|
||||||
|
attrs: {
|
||||||
|
spellcheck: 'false'
|
||||||
|
}
|
||||||
|
}, rightContent)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,9 @@ export default function referenceLink (h, cursor, block, token, outerClass) {
|
|||||||
)
|
)
|
||||||
const anchorSelector = href ? `a.${CLASS_OR_ID.AG_INLINE_RULE}.${CLASS_OR_ID.AG_REFERENCE_LINK}` : `span.${CLASS_OR_ID.AG_REFERENCE_LINK}`
|
const anchorSelector = href ? `a.${CLASS_OR_ID.AG_INLINE_RULE}.${CLASS_OR_ID.AG_REFERENCE_LINK}` : `span.${CLASS_OR_ID.AG_REFERENCE_LINK}`
|
||||||
const data = {
|
const data = {
|
||||||
|
attrs: {
|
||||||
|
spellcheck: 'false'
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
title
|
title
|
||||||
},
|
},
|
||||||
|
22
src/renderer/bootstrap.js
vendored
22
src/renderer/bootstrap.js
vendored
@ -27,6 +27,7 @@ const parseUrlArgs = () => {
|
|||||||
const userDataPath = params.get('udp')
|
const userDataPath = params.get('udp')
|
||||||
const windowId = Number(params.get('wid'))
|
const windowId = Number(params.get('wid'))
|
||||||
const type = params.get('type')
|
const type = params.get('type')
|
||||||
|
const spellcheckerIsHunspell = params.get('slp') === '1'
|
||||||
|
|
||||||
if (Number.isNaN(windowId)) {
|
if (Number.isNaN(windowId)) {
|
||||||
throw new Error('Error while parsing URL arguments: windowId!')
|
throw new Error('Error while parsing URL arguments: windowId!')
|
||||||
@ -43,7 +44,8 @@ const parseUrlArgs = () => {
|
|||||||
hideScrollbar,
|
hideScrollbar,
|
||||||
theme,
|
theme,
|
||||||
titleBarStyle
|
titleBarStyle
|
||||||
}
|
},
|
||||||
|
spellcheckerIsHunspell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +60,7 @@ const bootstrapRenderer = () => {
|
|||||||
|
|
||||||
// Register renderer exception handler
|
// Register renderer exception handler
|
||||||
window.addEventListener('error', event => {
|
window.addEventListener('error', event => {
|
||||||
|
if (event.error) {
|
||||||
const { message, name, stack } = event.error
|
const { message, name, stack } = event.error
|
||||||
const copy = {
|
const copy = {
|
||||||
message,
|
message,
|
||||||
@ -69,9 +72,19 @@ const bootstrapRenderer = () => {
|
|||||||
|
|
||||||
// Pass exception to main process exception handler to show a error dialog.
|
// Pass exception to main process exception handler to show a error dialog.
|
||||||
ipcRenderer.send('AGANI::handle-renderer-error', copy)
|
ipcRenderer.send('AGANI::handle-renderer-error', copy)
|
||||||
|
} else {
|
||||||
|
console.error(event)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const { debug, initialState, userDataPath, windowId, type } = parseUrlArgs()
|
const {
|
||||||
|
debug,
|
||||||
|
initialState,
|
||||||
|
userDataPath,
|
||||||
|
windowId,
|
||||||
|
type,
|
||||||
|
spellcheckerIsHunspell
|
||||||
|
} = parseUrlArgs()
|
||||||
const paths = new RendererPaths(userDataPath)
|
const paths = new RendererPaths(userDataPath)
|
||||||
const marktext = {
|
const marktext = {
|
||||||
initialState,
|
initialState,
|
||||||
@ -85,6 +98,11 @@ const bootstrapRenderer = () => {
|
|||||||
}
|
}
|
||||||
global.marktext = marktext
|
global.marktext = marktext
|
||||||
|
|
||||||
|
// Set option to always use Hunspell instead OS spell checker.
|
||||||
|
if (spellcheckerIsHunspell) {
|
||||||
|
process.env['SPELLCHECKER_PREFER_HUNSPELL'] = 1 // eslint-disable-line dot-notation
|
||||||
|
}
|
||||||
|
|
||||||
configureLogger()
|
configureLogger()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,6 +73,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { shell } from 'electron'
|
||||||
|
import log from 'electron-log'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import ViewImage from 'view-image'
|
import ViewImage from 'view-image'
|
||||||
import Muya from 'muya/lib'
|
import Muya from 'muya/lib'
|
||||||
@ -88,22 +90,22 @@ import FormatPicker from 'muya/lib/ui/formatPicker'
|
|||||||
import LinkTools from 'muya/lib/ui/linkTools'
|
import LinkTools from 'muya/lib/ui/linkTools'
|
||||||
import TableBarTools from 'muya/lib/ui/tableTools'
|
import TableBarTools from 'muya/lib/ui/tableTools'
|
||||||
import FrontMenu from 'muya/lib/ui/frontMenu'
|
import FrontMenu from 'muya/lib/ui/frontMenu'
|
||||||
import bus from '../../bus'
|
|
||||||
import Search from '../search'
|
import Search from '../search'
|
||||||
import { animatedScrollTo } from '../../util'
|
import bus from '@/bus'
|
||||||
import { addCommonStyle, setEditorWidth } from '../../util/theme'
|
|
||||||
import { guessClipboardFilePath } from '../../util/clipboard'
|
|
||||||
import { showContextMenu } from '../../contextMenu/editor'
|
|
||||||
import Printer from '@/services/printService'
|
|
||||||
import { DEFAULT_EDITOR_FONT_FAMILY } from '@/config'
|
import { DEFAULT_EDITOR_FONT_FAMILY } from '@/config'
|
||||||
import { moveImageToFolder, uploadImage } from '@/util/fileSystem'
|
import { showContextMenu } from '@/contextMenu/editor'
|
||||||
import notice from '@/services/notification'
|
import notice from '@/services/notification'
|
||||||
|
import Printer from '@/services/printService'
|
||||||
|
import { offsetToWordCursor, validateLineCursor, SpellChecker } from '@/spellchecker'
|
||||||
|
import { isOsx, animatedScrollTo } from '@/util'
|
||||||
|
import { moveImageToFolder, uploadImage } from '@/util/fileSystem'
|
||||||
|
import { guessClipboardFilePath } from '@/util/clipboard'
|
||||||
|
import { addCommonStyle, setEditorWidth } from '@/util/theme'
|
||||||
|
|
||||||
import 'muya/themes/default.css'
|
import 'muya/themes/default.css'
|
||||||
import '@/assets/themes/codemirror/one-dark.css'
|
import '@/assets/themes/codemirror/one-dark.css'
|
||||||
import 'view-image/lib/imgViewer.css'
|
import 'view-image/lib/imgViewer.css'
|
||||||
import CloseIcon from '@/assets/icons/close.svg'
|
import CloseIcon from '@/assets/icons/close.svg'
|
||||||
import { shell } from 'electron'
|
|
||||||
|
|
||||||
const STANDAR_Y = 320
|
const STANDAR_Y = 320
|
||||||
|
|
||||||
@ -147,6 +149,11 @@ export default {
|
|||||||
imageFolderPath: state => state.preferences.imageFolderPath,
|
imageFolderPath: state => state.preferences.imageFolderPath,
|
||||||
theme: state => state.preferences.theme,
|
theme: state => state.preferences.theme,
|
||||||
hideScrollbar: state => state.preferences.hideScrollbar,
|
hideScrollbar: state => state.preferences.hideScrollbar,
|
||||||
|
spellcheckerEnabled: state => state.preferences.spellcheckerEnabled,
|
||||||
|
spellcheckerIsHunspell: state => state.preferences.spellcheckerIsHunspell,
|
||||||
|
spellcheckerNoUnderline: state => state.preferences.spellcheckerNoUnderline,
|
||||||
|
spellcheckerAutoDetectLanguage: state => state.preferences.spellcheckerAutoDetectLanguage,
|
||||||
|
spellcheckerLanguage: state => state.preferences.spellcheckerLanguage,
|
||||||
|
|
||||||
currentFile: state => state.editor.currentFile,
|
currentFile: state => state.editor.currentFile,
|
||||||
|
|
||||||
@ -159,6 +166,8 @@ export default {
|
|||||||
data () {
|
data () {
|
||||||
this.defaultFontFamily = DEFAULT_EDITOR_FONT_FAMILY
|
this.defaultFontFamily = DEFAULT_EDITOR_FONT_FAMILY
|
||||||
this.CloseIcon = CloseIcon
|
this.CloseIcon = CloseIcon
|
||||||
|
// Helper to ignore changes when the spell check provider was changed in settings.
|
||||||
|
this.spellcheckerIgnorChanges = false
|
||||||
return {
|
return {
|
||||||
selectionChange: null,
|
selectionChange: null,
|
||||||
editor: null,
|
editor: null,
|
||||||
@ -309,6 +318,88 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
spellcheckerEnabled: function (value, oldValue) {
|
||||||
|
if (value !== oldValue) {
|
||||||
|
const { editor, spellchecker } = this
|
||||||
|
const { isInitialized } = spellchecker
|
||||||
|
|
||||||
|
// Set Muya's spellcheck container attribute.
|
||||||
|
editor.setOptions({ spellcheckEnabled: value })
|
||||||
|
|
||||||
|
// Spell check is available but not initialized.
|
||||||
|
if (value && !isInitialized) {
|
||||||
|
this.initSpellchecker()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable or disable spell checker.
|
||||||
|
if (isInitialized) {
|
||||||
|
if (value) {
|
||||||
|
this.enableSpellchecker()
|
||||||
|
} else {
|
||||||
|
spellchecker.disableSpellchecker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spellcheckerIsHunspell: function (value, oldValue) {
|
||||||
|
// Special case when the OS supports multiple spell checker because the
|
||||||
|
// language may be invalid (provider 1 may support language xyz
|
||||||
|
// but provider 2 not). Otherwise ignore this event.
|
||||||
|
const multiProviderSupported = isOsx
|
||||||
|
if (multiProviderSupported && value !== oldValue) {
|
||||||
|
const { spellchecker } = this
|
||||||
|
const { isHunspell } = spellchecker
|
||||||
|
if (value === isHunspell) {
|
||||||
|
this.spellcheckerIgnorChanges = false
|
||||||
|
|
||||||
|
// Apply language from settings that may have changed.
|
||||||
|
const { spellcheckerLanguage } = this
|
||||||
|
const { isEnabled, lang } = spellchecker
|
||||||
|
if (isEnabled && spellcheckerLanguage !== lang) {
|
||||||
|
this.switchSpellcheckLanguage(spellcheckerLanguage)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ignore all settings language changes that occur when another
|
||||||
|
// spell check provider is selected.
|
||||||
|
this.spellcheckerIgnorChanges = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spellcheckerNoUnderline: function (value, oldValue) {
|
||||||
|
if (value !== oldValue) {
|
||||||
|
const { editor, spellchecker } = this
|
||||||
|
|
||||||
|
// Set Muya's spellcheck container attribute.
|
||||||
|
editor.setOptions({ spellcheckEnabled: !value })
|
||||||
|
|
||||||
|
const { isEnabled } = spellchecker
|
||||||
|
if (isEnabled) {
|
||||||
|
spellchecker.isPassiveMode = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spellcheckerAutoDetectLanguage: function (value, oldValue) {
|
||||||
|
const { spellchecker } = this
|
||||||
|
const { isEnabled } = spellchecker
|
||||||
|
if (value !== oldValue && isEnabled) {
|
||||||
|
spellchecker.automaticallyIdentifyLanguages = value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spellcheckerLanguage: function (value, oldValue) {
|
||||||
|
const { spellchecker, spellcheckerIgnorChanges } = this
|
||||||
|
if (!spellcheckerIgnorChanges && value !== oldValue) {
|
||||||
|
const { isEnabled, isInvalidState } = spellchecker
|
||||||
|
if (isEnabled) {
|
||||||
|
this.switchSpellcheckLanguage(value)
|
||||||
|
} else if (isInvalidState) {
|
||||||
|
// Spell checker is in an invalid state due to a missing dictionary and
|
||||||
|
// therefore deactivated. We can safely enable the spell checker again
|
||||||
|
// with the new language.
|
||||||
|
this.enableSpellchecker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
currentFile: function (value, oldValue) {
|
currentFile: function (value, oldValue) {
|
||||||
if (value && value !== oldValue) {
|
if (value && value !== oldValue) {
|
||||||
this.scrollToCursor(0)
|
this.scrollToCursor(0)
|
||||||
@ -342,7 +433,8 @@ export default {
|
|||||||
frontmatterType,
|
frontmatterType,
|
||||||
hideQuickInsertHint,
|
hideQuickInsertHint,
|
||||||
editorLineWidth,
|
editorLineWidth,
|
||||||
theme
|
theme,
|
||||||
|
spellcheckerEnabled
|
||||||
} = this
|
} = this
|
||||||
|
|
||||||
// use muya UI plugins
|
// use muya UI plugins
|
||||||
@ -379,6 +471,7 @@ export default {
|
|||||||
listIndentation,
|
listIndentation,
|
||||||
frontmatterType,
|
frontmatterType,
|
||||||
hideQuickInsertHint,
|
hideQuickInsertHint,
|
||||||
|
spellcheckEnabled: spellcheckerEnabled,
|
||||||
imageAction: this.imageAction.bind(this),
|
imageAction: this.imageAction.bind(this),
|
||||||
imagePathPicker: this.imagePathPicker.bind(this),
|
imagePathPicker: this.imagePathPicker.bind(this),
|
||||||
clipboardFilePath: guessClipboardFilePath,
|
clipboardFilePath: guessClipboardFilePath,
|
||||||
@ -399,6 +492,12 @@ export default {
|
|||||||
|
|
||||||
const { container } = this.editor = new Muya(ele, options)
|
const { container } = this.editor = new Muya(ele, options)
|
||||||
|
|
||||||
|
// Create spell check wrapper and enable spell checking if prefered.
|
||||||
|
this.spellchecker = new SpellChecker(spellcheckerEnabled)
|
||||||
|
if (spellcheckerEnabled) {
|
||||||
|
this.initSpellchecker()
|
||||||
|
}
|
||||||
|
|
||||||
if (typewriter) {
|
if (typewriter) {
|
||||||
this.scrollToCursor()
|
this.scrollToCursor()
|
||||||
}
|
}
|
||||||
@ -429,6 +528,7 @@ export default {
|
|||||||
bus.$on('scroll-to-header', this.scrollToHeader)
|
bus.$on('scroll-to-header', this.scrollToHeader)
|
||||||
bus.$on('print', this.handlePrint)
|
bus.$on('print', this.handlePrint)
|
||||||
bus.$on('screenshot-captured', this.handleScreenShot)
|
bus.$on('screenshot-captured', this.handleScreenShot)
|
||||||
|
bus.$on('switch-spellchecker-language', this.switchSpellcheckLanguage)
|
||||||
|
|
||||||
this.editor.on('change', changes => {
|
this.editor.on('change', changes => {
|
||||||
// WORKAROUND: "id: 'muya'"
|
// WORKAROUND: "id: 'muya'"
|
||||||
@ -436,7 +536,6 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.editor.on('format-click', ({ event, formatType, data }) => {
|
this.editor.on('format-click', ({ event, formatType, data }) => {
|
||||||
const isOsx = this.platform === 'darwin'
|
|
||||||
const ctrlOrMeta = (isOsx && event.metaKey) || (!isOsx && event.ctrlKey)
|
const ctrlOrMeta = (isOsx && event.metaKey) || (!isOsx && event.ctrlKey)
|
||||||
if (formatType === 'link' && ctrlOrMeta) {
|
if (formatType === 'link' && ctrlOrMeta) {
|
||||||
this.$store.dispatch('FORMAT_LINK_CLICK', { data, dirname: window.DIRNAME })
|
this.$store.dispatch('FORMAT_LINK_CLICK', { data, dirname: window.DIRNAME })
|
||||||
@ -481,8 +580,35 @@ export default {
|
|||||||
this.$store.dispatch('SELECTION_FORMATS', formats)
|
this.$store.dispatch('SELECTION_FORMATS', formats)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.editor.on('contextmenu', (event, selectionChanges) => {
|
this.editor.on('contextmenu', (event, selection) => {
|
||||||
showContextMenu(event, selectionChanges)
|
const { isEnabled } = this.spellchecker
|
||||||
|
|
||||||
|
// NOTE: Right clicking on a misspelled word select the whole word
|
||||||
|
// by Chromium.
|
||||||
|
if (isEnabled && validateLineCursor(selection)) {
|
||||||
|
const { start: startCursor } = selection
|
||||||
|
const { offset: lineOffset } = startCursor
|
||||||
|
const { text } = startCursor.block
|
||||||
|
const wordInfo = SpellChecker.extractWord(text, lineOffset)
|
||||||
|
if (wordInfo) {
|
||||||
|
const { left, right, word } = wordInfo
|
||||||
|
|
||||||
|
// Translate offsets into a cursor with the given line.
|
||||||
|
const wordRange = offsetToWordCursor(selection, left, right)
|
||||||
|
this.spellchecker.getWordSuggestion(word)
|
||||||
|
.then(wordSuggestions => {
|
||||||
|
const replaceCallback = replacement => {
|
||||||
|
// wordRange := replace this range with the replacement
|
||||||
|
this.editor.replaceWordInline(selection, wordRange, replacement, true)
|
||||||
|
}
|
||||||
|
showContextMenu(event, selection, this.spellchecker, word, wordSuggestions, replaceCallback)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No word selected or fallback
|
||||||
|
showContextMenu(event, selection, isEnabled ? this.spellchecker : null, '', [], null)
|
||||||
})
|
})
|
||||||
|
|
||||||
document.addEventListener('keyup', this.keyup)
|
document.addEventListener('keyup', this.keyup)
|
||||||
@ -496,7 +622,6 @@ export default {
|
|||||||
},
|
},
|
||||||
jumpClick: (linkInfo) => {
|
jumpClick: (linkInfo) => {
|
||||||
const { href } = linkInfo
|
const { href } = linkInfo
|
||||||
|
|
||||||
if (href && href.startsWith('http')) {
|
if (href && href.startsWith('http')) {
|
||||||
shell.openExternal(href)
|
shell.openExternal(href)
|
||||||
}
|
}
|
||||||
@ -564,6 +689,107 @@ export default {
|
|||||||
this.imageViewerVisible = status
|
this.imageViewerVisible = status
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Helper methods for spell checker that are needed multiple times.
|
||||||
|
initSpellchecker () {
|
||||||
|
const {
|
||||||
|
editor,
|
||||||
|
spellchecker,
|
||||||
|
spellcheckerNoUnderline,
|
||||||
|
spellcheckerAutoDetectLanguage,
|
||||||
|
spellcheckerLanguage
|
||||||
|
} = this
|
||||||
|
const { container } = editor
|
||||||
|
|
||||||
|
spellchecker.init(
|
||||||
|
spellcheckerLanguage,
|
||||||
|
spellcheckerAutoDetectLanguage,
|
||||||
|
spellcheckerNoUnderline,
|
||||||
|
container
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
log.error(`Error while initializing spell checker for language "${spellcheckerLanguage}":`)
|
||||||
|
log.error(error)
|
||||||
|
|
||||||
|
notice.notify({
|
||||||
|
title: 'Spelling',
|
||||||
|
type: 'error',
|
||||||
|
message: `Error while initializing spell checker for language "${spellcheckerLanguage}": ${error.message}`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
enableSpellchecker () {
|
||||||
|
const {
|
||||||
|
spellchecker,
|
||||||
|
spellcheckerNoUnderline,
|
||||||
|
spellcheckerAutoDetectLanguage,
|
||||||
|
spellcheckerLanguage
|
||||||
|
} = this
|
||||||
|
|
||||||
|
spellchecker.enableSpellchecker(
|
||||||
|
spellcheckerLanguage,
|
||||||
|
spellcheckerAutoDetectLanguage,
|
||||||
|
spellcheckerNoUnderline
|
||||||
|
)
|
||||||
|
.then(status => {
|
||||||
|
if (!status) {
|
||||||
|
// Unable to switch language due to missing dictionary. The spell checker is now in an invalid state.
|
||||||
|
notice.notify({
|
||||||
|
title: 'Spelling',
|
||||||
|
type: 'warning',
|
||||||
|
message: `Unable to switch to language "${spellcheckerLanguage}". Spell checker is now disabled.`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
log.error(`Error while enabling spell checking for "${spellcheckerLanguage}":`)
|
||||||
|
log.error(error)
|
||||||
|
|
||||||
|
notice.notify({
|
||||||
|
title: 'Spelling',
|
||||||
|
type: 'error',
|
||||||
|
message: `Error while enabling spell checking for "${spellcheckerLanguage}": ${error.message}`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
switchSpellcheckLanguage (languageCode) {
|
||||||
|
const { spellchecker } = this
|
||||||
|
const { isEnabled } = spellchecker
|
||||||
|
|
||||||
|
// This method is also called from bus, so validate state before continuing.
|
||||||
|
if (!isEnabled) {
|
||||||
|
throw new Error('Cannot switch switch because spell checker is disabled!')
|
||||||
|
}
|
||||||
|
|
||||||
|
spellchecker.switchLanguage(languageCode)
|
||||||
|
.then(langCode => {
|
||||||
|
if (!langCode) {
|
||||||
|
// Unable to switch language due to missing dictionary. The spell checker is now in an invalid state.
|
||||||
|
notice.notify({
|
||||||
|
title: 'Spelling',
|
||||||
|
type: 'warning',
|
||||||
|
message: `Unable to switch to language "${languageCode}". Spell checker is now disabled.`
|
||||||
|
})
|
||||||
|
} else if (langCode !== languageCode) {
|
||||||
|
// Unable to switch language but fallback was successful.
|
||||||
|
notice.notify({
|
||||||
|
title: 'Spelling',
|
||||||
|
type: 'warning',
|
||||||
|
message: `Current spelling language is "${langCode}" because "${languageCode}" dictionary is missing.`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
log.error(`Error while switching to language "${languageCode}":`)
|
||||||
|
log.error(error)
|
||||||
|
|
||||||
|
notice.notify({
|
||||||
|
title: 'Spelling',
|
||||||
|
type: 'error',
|
||||||
|
message: `Error while switching to "${languageCode}": ${error.message}`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
handleUndo () {
|
handleUndo () {
|
||||||
if (this.editor) {
|
if (this.editor) {
|
||||||
this.editor.undo()
|
this.editor.undo()
|
||||||
@ -789,6 +1015,7 @@ export default {
|
|||||||
bus.$off('scroll-to-header', this.scrollToHeader)
|
bus.$off('scroll-to-header', this.scrollToHeader)
|
||||||
bus.$off('print', this.handlePrint)
|
bus.$off('print', this.handlePrint)
|
||||||
bus.$off('screenshot-captured', this.handleScreenShot)
|
bus.$off('screenshot-captured', this.handleScreenShot)
|
||||||
|
bus.$off('switch-spellchecker-language', this.switchSpellcheckLanguage)
|
||||||
|
|
||||||
document.removeEventListener('keyup', this.keyup)
|
document.removeEventListener('keyup', this.keyup)
|
||||||
|
|
||||||
|
@ -10,16 +10,37 @@ import {
|
|||||||
INSERT_BEFORE,
|
INSERT_BEFORE,
|
||||||
INSERT_AFTER
|
INSERT_AFTER
|
||||||
} from './menuItems'
|
} from './menuItems'
|
||||||
|
import spellcheckMenuBuilder from './spellcheck'
|
||||||
|
|
||||||
const { Menu, MenuItem } = remote
|
const { Menu, MenuItem } = remote
|
||||||
|
|
||||||
export const showContextMenu = (event, { start, end }) => {
|
/**
|
||||||
|
* Show editor context menu.
|
||||||
|
*
|
||||||
|
* @param {MouseEvent} event The native mouse event.
|
||||||
|
* @param {*} selection The editor line with start and end offset.
|
||||||
|
* @param {[SpellChecker]} spellchecker The spellcheck wrapper.
|
||||||
|
* @param {[string]} selectedWord The selected word.
|
||||||
|
* @param {[string[]]} wordSuggestions Suggestions for `word`.
|
||||||
|
* @param {*} replaceCallback The callback to replace the word by a replacement.
|
||||||
|
*/
|
||||||
|
export const showContextMenu = (event, selection, spellchecker, selectedWord, wordSuggestions, replaceCallback) => {
|
||||||
|
const { start, end } = selection
|
||||||
const menu = new Menu()
|
const menu = new Menu()
|
||||||
const win = remote.getCurrentWindow()
|
const win = remote.getCurrentWindow()
|
||||||
const disableCutAndCopy = start.key === end.key && start.offset === end.offset
|
const disableCutAndCopy = start.key === end.key && start.offset === end.offset
|
||||||
const CONTEXT_ITEMS = [INSERT_BEFORE, INSERT_AFTER, SEPARATOR, CUT, COPY, PASTE, SEPARATOR, COPY_AS_MARKDOWN, COPY_AS_HTML, PASTE_AS_PLAIN_TEXT]
|
const CONTEXT_ITEMS = [INSERT_BEFORE, INSERT_AFTER, SEPARATOR, CUT, COPY, PASTE, SEPARATOR, COPY_AS_MARKDOWN, COPY_AS_HTML, PASTE_AS_PLAIN_TEXT]
|
||||||
|
|
||||||
;[CUT, COPY, COPY_AS_HTML, COPY_AS_MARKDOWN].forEach(item => {
|
const spellingSubmenu = spellcheckMenuBuilder(spellchecker, selectedWord, wordSuggestions, replaceCallback)
|
||||||
|
if (spellingSubmenu) {
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: 'Spelling...',
|
||||||
|
submenu: spellingSubmenu
|
||||||
|
}))
|
||||||
|
menu.append(new MenuItem(SEPARATOR))
|
||||||
|
}
|
||||||
|
|
||||||
|
[CUT, COPY, COPY_AS_HTML, COPY_AS_MARKDOWN].forEach(item => {
|
||||||
item.enabled = !disableCutAndCopy
|
item.enabled = !disableCutAndCopy
|
||||||
})
|
})
|
||||||
|
|
||||||
|
95
src/renderer/contextMenu/editor/spellcheck.js
Normal file
95
src/renderer/contextMenu/editor/spellcheck.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { remote } from 'electron'
|
||||||
|
import log from 'electron-log'
|
||||||
|
import bus from '@/bus'
|
||||||
|
import { SEPARATOR } from './menuItems'
|
||||||
|
|
||||||
|
const { MenuItem } = remote
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the spell checker menu depending on input.
|
||||||
|
*
|
||||||
|
* @param {[SpellChecker]} spellchecker The spellcheck wrapper.
|
||||||
|
* @param {[string]} selectedWord The selected word.
|
||||||
|
* @param {[string[]]} wordSuggestions Suggestions for `word`.
|
||||||
|
* @param {*} replaceCallback The callback to replace the word by a replacement.
|
||||||
|
* @returns {MenuItem[]}
|
||||||
|
*/
|
||||||
|
export default (spellchecker, selectedWord, wordSuggestions, replaceCallback) => {
|
||||||
|
if (spellchecker && spellchecker.isEnabled) {
|
||||||
|
const spellingSubmenu = []
|
||||||
|
|
||||||
|
// Change language menu entries
|
||||||
|
const currentLanguage = spellchecker.lang
|
||||||
|
const availableDictionaries = spellchecker.getAvailableDictionaries()
|
||||||
|
const availableDictionariesSubmenu = []
|
||||||
|
for (const dict of availableDictionaries) {
|
||||||
|
availableDictionariesSubmenu.push(new MenuItem({
|
||||||
|
label: dict,
|
||||||
|
enabled: dict !== currentLanguage,
|
||||||
|
click () {
|
||||||
|
bus.$emit('switch-spellchecker-language', dict)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
spellingSubmenu.push(new MenuItem({
|
||||||
|
label: 'Change Language...',
|
||||||
|
submenu: availableDictionariesSubmenu
|
||||||
|
}))
|
||||||
|
|
||||||
|
spellingSubmenu.push(SEPARATOR)
|
||||||
|
|
||||||
|
// Word suggestions
|
||||||
|
if (selectedWord && wordSuggestions && wordSuggestions.length > 0) {
|
||||||
|
spellingSubmenu.push({
|
||||||
|
label: 'Add to Dictionary',
|
||||||
|
click (menuItem, targetWindow) {
|
||||||
|
// NOTE: Need to notify Chromium to invalidate the spelling underline.
|
||||||
|
targetWindow.webContents.replaceMisspelling(selectedWord)
|
||||||
|
spellchecker.addToDictionary(selectedWord)
|
||||||
|
.catch(error => {
|
||||||
|
log.error(`Error while adding "${selectedWord}" to dictionary.`)
|
||||||
|
log.error(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Ignore word for current runtime for all languages.
|
||||||
|
spellingSubmenu.push({
|
||||||
|
label: 'Ignore',
|
||||||
|
click (menuItem, targetWindow) {
|
||||||
|
// NOTE: Need to notify Chromium to invalidate the spelling underline.
|
||||||
|
targetWindow.webContents.replaceMisspelling(selectedWord)
|
||||||
|
spellchecker.ignoreWord(selectedWord)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
spellingSubmenu.push(SEPARATOR)
|
||||||
|
for (const word of wordSuggestions) {
|
||||||
|
spellingSubmenu.push({
|
||||||
|
label: word,
|
||||||
|
click () {
|
||||||
|
// Notify Muya to replace the word. We cannot just use Chromium to
|
||||||
|
// replace the word because the change is not forwarded to Muya.
|
||||||
|
replaceCallback(word)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
spellingSubmenu.push({
|
||||||
|
label: 'Remove from Dictionary',
|
||||||
|
// NOTE: We cannot validate that the word is inside the user dictionary.
|
||||||
|
enabled: !!selectedWord && selectedWord.length >= 2,
|
||||||
|
click (menuItem, targetWindow) {
|
||||||
|
// NOTE: Need to notify Chromium to invalidate the spelling underline.
|
||||||
|
targetWindow.webContents.replaceMisspelling(selectedWord)
|
||||||
|
spellchecker.removeFromDictionary(selectedWord)
|
||||||
|
.catch(error => {
|
||||||
|
log.error(`Error while removing "${selectedWord}" from dictionary.`)
|
||||||
|
log.error(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return spellingSubmenu
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
@ -28,6 +28,8 @@ import {
|
|||||||
Option,
|
Option,
|
||||||
Radio,
|
Radio,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
|
Table,
|
||||||
|
TableColumn,
|
||||||
Tabs,
|
Tabs,
|
||||||
TabPane,
|
TabPane,
|
||||||
Input
|
Input
|
||||||
@ -78,6 +80,8 @@ Vue.use(Select)
|
|||||||
Vue.use(Option)
|
Vue.use(Option)
|
||||||
Vue.use(Radio)
|
Vue.use(Radio)
|
||||||
Vue.use(RadioGroup)
|
Vue.use(RadioGroup)
|
||||||
|
Vue.use(Table)
|
||||||
|
Vue.use(TableColumn)
|
||||||
Vue.use(Tabs)
|
Vue.use(Tabs)
|
||||||
Vue.use(TabPane)
|
Vue.use(TabPane)
|
||||||
Vue.use(Input)
|
Vue.use(Input)
|
||||||
|
@ -22,6 +22,11 @@ export const category = [{
|
|||||||
label: 'markdown',
|
label: 'markdown',
|
||||||
icon: MarkdownIcon,
|
icon: MarkdownIcon,
|
||||||
path: '/preference/markdown'
|
path: '/preference/markdown'
|
||||||
|
}, {
|
||||||
|
name: 'Spelling',
|
||||||
|
label: 'spelling',
|
||||||
|
icon: GeneralIcon, // TODO: replace icon
|
||||||
|
path: '/preference/spelling'
|
||||||
}, {
|
}, {
|
||||||
name: 'Theme',
|
name: 'Theme',
|
||||||
label: 'theme',
|
label: 'theme',
|
||||||
|
326
src/renderer/prefComponents/spellchecker/index.vue
Normal file
326
src/renderer/prefComponents/spellchecker/index.vue
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pref-spellchecker">
|
||||||
|
<h4>Spelling</h4>
|
||||||
|
<bool
|
||||||
|
description="Whether the experimental spell checker is enabled to check for spelling mistakes."
|
||||||
|
:bool="spellcheckerEnabled"
|
||||||
|
:onChange="value => onSelectChange('spellcheckerEnabled', value)"
|
||||||
|
></bool>
|
||||||
|
<separator></separator>
|
||||||
|
<bool
|
||||||
|
description="When enabled Hunspell is used instead the OS spell checker (macOS only). The change take effect after application restart or for new editor windows."
|
||||||
|
:bool="spellcheckerIsHunspell"
|
||||||
|
:disable="!isOsx || !spellcheckerEnabled"
|
||||||
|
:onChange="value => onSelectChange('spellcheckerIsHunspell', value)"
|
||||||
|
></bool>
|
||||||
|
<bool
|
||||||
|
description="Don't underline spelling mistakes. You can still correct spelling mistakes via right click menu."
|
||||||
|
:bool="spellcheckerNoUnderline"
|
||||||
|
:disable="!spellcheckerEnabled"
|
||||||
|
:onChange="value => onSelectChange('spellcheckerNoUnderline', value)"
|
||||||
|
></bool>
|
||||||
|
<bool
|
||||||
|
description="Try to automatically identify the used language when typing. This feature is currently not available for Hunspell or when spelling mistakes are not underlined."
|
||||||
|
:bool="spellcheckerAutoDetectLanguage"
|
||||||
|
:disable="!spellcheckerEnabled"
|
||||||
|
:onChange="value => onSelectChange('spellcheckerAutoDetectLanguage', value)"
|
||||||
|
></bool>
|
||||||
|
<cur-select
|
||||||
|
description="The default language for spelling."
|
||||||
|
:value="spellcheckerLanguage"
|
||||||
|
:options="availableDictionaries"
|
||||||
|
:disable="!spellcheckerEnabled"
|
||||||
|
:onChange="value => onSelectChange('spellcheckerLanguage', value)"
|
||||||
|
></cur-select>
|
||||||
|
<div
|
||||||
|
v-if="isOsx && spellcheckerIsHunspell"
|
||||||
|
class="description"
|
||||||
|
>
|
||||||
|
Please add the needed language dictionaries via Language & Region in your system preferences pane.
|
||||||
|
</div>
|
||||||
|
<div v-if="isHunspellSelected && spellcheckerEnabled">
|
||||||
|
<separator></separator>
|
||||||
|
<div class="description">Available Hunspell dictionaries. Please add additional language dictionaries via button below.</div>
|
||||||
|
<el-table
|
||||||
|
:data="availableDictionaries"
|
||||||
|
style="width: 100%">
|
||||||
|
<el-table-column
|
||||||
|
prop="value"
|
||||||
|
label="Name"
|
||||||
|
width="100">
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="label"
|
||||||
|
label="Language"
|
||||||
|
>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
fixed="right"
|
||||||
|
label="Operations"
|
||||||
|
width="170">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button @click="handleUpdateClick(scope.$index, scope.row)" type="text" size="small">Update</el-button>
|
||||||
|
<el-button @click="handleDeleteClick(scope.$index, scope.row)" type="text" size="small">Delete</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="description">Add new dictionaries to Hunspell.</div>
|
||||||
|
<div class="dictionary-group">
|
||||||
|
<el-select
|
||||||
|
v-model="selectedDictionaryToAdd"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in dictionariesLanguagesOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
<el-button icon="el-icon-document-add" @click="addNewDict"></el-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="errorMessage" class="description">{{ errorMessage }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import CurSelect from '../common/select'
|
||||||
|
import Bool from '../common/bool'
|
||||||
|
import Separator from '../common/separator'
|
||||||
|
import { isOsx } from '@/util'
|
||||||
|
import { getAvailableHunspellDictionaries, SpellChecker } from '@/spellchecker'
|
||||||
|
import { getLanguageName, HUNSPELL_DICTIONARY_LANGUAGE_MAP } from '@/spellchecker/languageMap.js'
|
||||||
|
import { downloadHunspellDictionary, deleteHunspellDictionary } from '@/spellchecker/dictionaryDownloader'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Bool,
|
||||||
|
CurSelect,
|
||||||
|
Separator
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
this.isOsx = isOsx
|
||||||
|
this.dictionariesLanguagesOptions = HUNSPELL_DICTIONARY_LANGUAGE_MAP
|
||||||
|
this.hunspellDictionaryDownloadCache = {}
|
||||||
|
return {
|
||||||
|
availableDictionaries: [],
|
||||||
|
selectedDictionaryToAdd: 'en-US',
|
||||||
|
errorMessage: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
spellcheckerEnabled: state => state.preferences.spellcheckerEnabled,
|
||||||
|
spellcheckerIsHunspell: state => state.preferences.spellcheckerIsHunspell,
|
||||||
|
spellcheckerNoUnderline: state => state.preferences.spellcheckerNoUnderline,
|
||||||
|
spellcheckerAutoDetectLanguage: state => state.preferences.spellcheckerAutoDetectLanguage,
|
||||||
|
spellcheckerLanguage: state => state.preferences.spellcheckerLanguage,
|
||||||
|
isHunspellSelected: state => {
|
||||||
|
return !isOsx || state.preferences.spellcheckerIsHunspell
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
spellcheckerIsHunspell: function (value, oldValue) {
|
||||||
|
if (isOsx && value !== oldValue && value) {
|
||||||
|
const { spellcheckerLanguage } = this
|
||||||
|
const index = HUNSPELL_DICTIONARY_LANGUAGE_MAP.findIndex(d => d.value === spellcheckerLanguage)
|
||||||
|
if (index === -1) {
|
||||||
|
// Language is not supported by Hunspell.
|
||||||
|
this.onSelectChange('spellcheckerLanguage', 'en-US')
|
||||||
|
}
|
||||||
|
this.refreshDictionaryList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.refreshDictionaryList()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
if (isOsx && this.spellChecker) {
|
||||||
|
this.spellChecker.provider.unsubscribe()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getAvailableDictionaries () {
|
||||||
|
let dictionaries = []
|
||||||
|
if (this.isHunspellSelected) {
|
||||||
|
// Search hunspell dictionaries on disk.
|
||||||
|
dictionaries = getAvailableHunspellDictionaries()
|
||||||
|
} else {
|
||||||
|
// On macOS we only receive the dictionaries when the spell checker is active.
|
||||||
|
if (!this.spellChecker) {
|
||||||
|
// Create a new spell checker provider without attach it.
|
||||||
|
this.spellChecker = new SpellChecker()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive available dictionaries from OS.
|
||||||
|
dictionaries = this.spellChecker.getAvailableDictionaries()
|
||||||
|
}
|
||||||
|
|
||||||
|
return dictionaries.map(item => {
|
||||||
|
return {
|
||||||
|
value: item,
|
||||||
|
label: getLanguageName(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
refreshDictionaryList () {
|
||||||
|
this.availableDictionaries = this.getAvailableDictionaries()
|
||||||
|
},
|
||||||
|
onSelectChange (type, value) {
|
||||||
|
this.$store.dispatch('SET_SINGLE_PREFERENCE', { type, value })
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Hunspell only ------------------------------------------------------
|
||||||
|
|
||||||
|
addNewDict () {
|
||||||
|
const { selectedDictionaryToAdd } = this
|
||||||
|
if (!this.isHunspellDictionaryAvailable(selectedDictionaryToAdd)) {
|
||||||
|
this.startDownloadHunspellDictionary(selectedDictionaryToAdd)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleUpdateClick (index, row) {
|
||||||
|
this.startDownloadHunspellDictionary(row.value)
|
||||||
|
},
|
||||||
|
handleDeleteClick (index, row) {
|
||||||
|
const { spellcheckerLanguage } = this
|
||||||
|
const { value: lang } = row
|
||||||
|
|
||||||
|
// Don't allow to delete our fallback language.
|
||||||
|
if (lang === 'en-US') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback before deleting selected language.
|
||||||
|
if (spellcheckerLanguage === lang) {
|
||||||
|
this.onSelectChange('spellcheckerLanguage', 'en-US')
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteHunspellDictionary(lang)
|
||||||
|
.then(() => {
|
||||||
|
this.refreshDictionaryList()
|
||||||
|
}).catch(error => {
|
||||||
|
this.errorMessage = `Error deleting dictionary: ${error.message}`
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
startDownloadHunspellDictionary (languageCode) {
|
||||||
|
if (this.hunspellDictionaryDownloadCache[languageCode]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hunspellDictionaryDownloadCache[languageCode] = 1
|
||||||
|
downloadHunspellDictionary(languageCode)
|
||||||
|
.then(() => {
|
||||||
|
delete this.hunspellDictionaryDownloadCache[languageCode]
|
||||||
|
this.refreshDictionaryList()
|
||||||
|
}).catch(error => {
|
||||||
|
delete this.hunspellDictionaryDownloadCache[languageCode]
|
||||||
|
this.errorMessage = `Error while downloading: ${error.message}`
|
||||||
|
})
|
||||||
|
},
|
||||||
|
isHunspellDictionaryAvailable (languageCode) {
|
||||||
|
const { availableDictionaries } = this
|
||||||
|
return availableDictionaries.findIndex(d => d.value === languageCode) !== -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pref-spellchecker {
|
||||||
|
& h4 {
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
& div.description {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
color: var(--iconColor);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-table, .el-table__expanded-cell {
|
||||||
|
background: var(--editorBgColor);
|
||||||
|
}
|
||||||
|
.el-table button {
|
||||||
|
padding: 1px 2px;
|
||||||
|
margin: 5px 10px;
|
||||||
|
color: var(--themeColor);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.el-table button:hover,
|
||||||
|
.el-table button:active {
|
||||||
|
opacity: 0.9;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.dictionary-group {
|
||||||
|
display: flex;
|
||||||
|
& button.el-button {
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
.pref-spellchecker .el-table table {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.pref-spellchecker .el-table th,
|
||||||
|
.pref-spellchecker .el-table tr {
|
||||||
|
background: var(--editorBgColor);
|
||||||
|
}
|
||||||
|
.pref-spellchecker .el-table td,
|
||||||
|
.pref-spellchecker .el-table th.is-leaf {
|
||||||
|
border: 1px solid var(--tableBorderColor);
|
||||||
|
}
|
||||||
|
.pref-spellchecker .el-table--border::after,
|
||||||
|
.pref-spellchecker .el-table--group::after,
|
||||||
|
.pref-spellchecker .el-table::before,
|
||||||
|
.pref-spellchecker .el-table__fixed-right::before,
|
||||||
|
.pref-spellchecker .el-table__fixed::before {
|
||||||
|
background: var(--tableBorderColor);
|
||||||
|
}
|
||||||
|
.pref-spellchecker .el-table__body tr.hover-row.current-row>td,
|
||||||
|
.pref-spellchecker .el-table__body tr.hover-row.el-table__row--striped.current-row>td,
|
||||||
|
.pref-spellchecker .el-table__body tr.hover-row.el-table__row--striped>td,
|
||||||
|
.pref-spellchecker .el-table__body tr.hover-row>td {
|
||||||
|
background: var(--selectionColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pref-spellchecker li.el-select-dropdown__item {
|
||||||
|
color: var(--editorColor);
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
.pref-spellchecker li.el-select-dropdown__item.hover, li.el-select-dropdown__item:hover {
|
||||||
|
background: var(--floatHoverColor);
|
||||||
|
}
|
||||||
|
.pref-spellchecker div.el-select-dropdown {
|
||||||
|
background: var(--floatBgColor);
|
||||||
|
border-color: var(--floatBorderColor);
|
||||||
|
& .popper__arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pref-spellchecker input.el-input__inner {
|
||||||
|
height: 30px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--editorColor);
|
||||||
|
border-color: var(--editorColor10);
|
||||||
|
}
|
||||||
|
.pref-spellchecker .el-input__icon,
|
||||||
|
.pref-spellchecker .el-input__inner {
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -3,6 +3,7 @@ import Preference from '@/pages/preference'
|
|||||||
import General from '@/prefComponents/general'
|
import General from '@/prefComponents/general'
|
||||||
import Editor from '@/prefComponents/editor'
|
import Editor from '@/prefComponents/editor'
|
||||||
import Markdown from '@/prefComponents/markdown'
|
import Markdown from '@/prefComponents/markdown'
|
||||||
|
import SpellChecker from '@/prefComponents/spellchecker'
|
||||||
import Theme from '@/prefComponents/theme'
|
import Theme from '@/prefComponents/theme'
|
||||||
import Image from '@/prefComponents/image'
|
import Image from '@/prefComponents/image'
|
||||||
import ImageUploader from '@/prefComponents/imageUploader'
|
import ImageUploader from '@/prefComponents/imageUploader'
|
||||||
@ -22,6 +23,8 @@ const routes = type => ([{
|
|||||||
path: 'editor', component: Editor, name: 'editor'
|
path: 'editor', component: Editor, name: 'editor'
|
||||||
}, {
|
}, {
|
||||||
path: 'markdown', component: Markdown, name: 'markdown'
|
path: 'markdown', component: Markdown, name: 'markdown'
|
||||||
|
}, {
|
||||||
|
path: 'spelling', component: SpellChecker, name: 'spelling'
|
||||||
}, {
|
}, {
|
||||||
path: 'theme', component: Theme, name: 'theme'
|
path: 'theme', component: Theme, name: 'theme'
|
||||||
}, {
|
}, {
|
||||||
|
35
src/renderer/spellchecker/dictionaryDownloader.js
Normal file
35
src/renderer/spellchecker/dictionaryDownloader.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import fs from 'fs-extra'
|
||||||
|
import path from 'path'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { SpellChecker } from '@hfelix/electron-spellchecker'
|
||||||
|
import { dictionaryPath } from '../spellchecker'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to download the given Hunspell dictionary.
|
||||||
|
*
|
||||||
|
* @param {string} lang The language to download.
|
||||||
|
*/
|
||||||
|
export const downloadHunspellDictionary = async lang => {
|
||||||
|
const url = SpellChecker.getURLForHunspellDictionary(lang)
|
||||||
|
const response = await axios({
|
||||||
|
method: 'get',
|
||||||
|
url,
|
||||||
|
responseType: 'stream'
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const outStream = fs.createWriteStream(path.join(dictionaryPath, `${lang}.bdic`))
|
||||||
|
response.data.pipe(outStream)
|
||||||
|
outStream.once('error', reject)
|
||||||
|
outStream.once('finish', () => resolve())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the given Hunspell dictionary from disk.
|
||||||
|
*
|
||||||
|
* @param {string} lang The language to remove.
|
||||||
|
*/
|
||||||
|
export const deleteHunspellDictionary = async lang => {
|
||||||
|
return await fs.remove(path.join(dictionaryPath, `${lang}.bdic`))
|
||||||
|
}
|
486
src/renderer/spellchecker/index.js
Normal file
486
src/renderer/spellchecker/index.js
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
import fs from 'fs-extra'
|
||||||
|
import path from 'path'
|
||||||
|
import { remote } from 'electron'
|
||||||
|
import { SpellCheckHandler, fallbackLocales, normalizeLanguageCode } from '@hfelix/electron-spellchecker'
|
||||||
|
import { isOsx, cloneObj } from '../util'
|
||||||
|
|
||||||
|
// NOTE: Hardcoded in "@hfelix/electron-spellchecker/src/spell-check-handler.js"
|
||||||
|
export const dictionaryPath = path.join(remote.app.getPath('userData'), 'dictionaries')
|
||||||
|
|
||||||
|
// Source: https://github.com/Microsoft/vscode/blob/master/src/vs/editor/common/model/wordHelper.ts
|
||||||
|
// /(-?\d*\.\d\w*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/
|
||||||
|
const WORD_SEPARATORS = /([`~!@#$%^&*()-=+[{\]}\\|;:'",.<>/?\s])/
|
||||||
|
const WORD_DEFINITION = /(-?\d*\.\d\w*)|([^`~!@#$%^&*()-=+[{\]}\\|;:'",.<>/?\s]+)$/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate a left and right offset from a word in `line` into a cursor with
|
||||||
|
* the given line cursor.
|
||||||
|
*
|
||||||
|
* @param {*} lineCursor The original line cursor.
|
||||||
|
* @param {number} left Start offset/index of word in `lineCursor`.
|
||||||
|
* @param {number} right End offset/index of word in `lineCursor`.
|
||||||
|
* @returns {*} Return a cursor of the word selected in `lineCursor`(e.g.
|
||||||
|
* "foo >bar< foo" where `>`/`<` start and end offset).
|
||||||
|
*/
|
||||||
|
export const offsetToWordCursor = (lineCursor, left, right) => {
|
||||||
|
// Deep clone cursor start and end
|
||||||
|
const start = cloneObj(lineCursor.start, true)
|
||||||
|
const end = cloneObj(lineCursor.end, true)
|
||||||
|
start.offset = left
|
||||||
|
end.offset = right
|
||||||
|
return { start, end }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate whether the selection is valid for spelling correction.
|
||||||
|
*
|
||||||
|
* @param {*} selection The preview editor selection range.
|
||||||
|
*/
|
||||||
|
export const validateLineCursor = selection => {
|
||||||
|
// Validate selection range.
|
||||||
|
if (!selection && !selection.start && !selection.start.hasOwnProperty('offset') &&
|
||||||
|
!selection.end && !selection.end.hasOwnProperty('offset')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow only single lines
|
||||||
|
const { start: startCursor, end: endCursor } = selection
|
||||||
|
if (startCursor.key !== endCursor.key || !startCursor.block) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't correct words in code blocks or editors for HTML, LaTex and diagrams.
|
||||||
|
if (startCursor.block.functionType === 'codeContent' &&
|
||||||
|
startCursor.block.lang !== undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't correct words in code blocks or pre elements such as language identifier.
|
||||||
|
if (selection.affiliation && selection.affiliation.length === 1 &&
|
||||||
|
selection.affiliation[0].type === 'pre') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of local available Hunspell dictionaries.
|
||||||
|
*
|
||||||
|
* @returns {string[]} List of available Hunspell dictionary language codes.
|
||||||
|
*/
|
||||||
|
export const getAvailableHunspellDictionaries = () => {
|
||||||
|
const dict = []
|
||||||
|
// Search for dictionaries on filesystem.
|
||||||
|
if (fs.existsSync(dictionaryPath) && fs.lstatSync(dictionaryPath).isDirectory()) {
|
||||||
|
fs.readdirSync(dictionaryPath).forEach(filename => {
|
||||||
|
const fullname = path.join(dictionaryPath, filename)
|
||||||
|
const match = filename.match(/^([a-z]{2}(?:[-][A-Z]{2})?)\.bdic$/)
|
||||||
|
if (match && match[1] && fs.lstatSync(fullname).isFile()) {
|
||||||
|
dict.push(match[1])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return dict
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High level spell checker API.
|
||||||
|
*
|
||||||
|
* Language providers:
|
||||||
|
* - macOS: NSSpellChecker (default) or Hunspell
|
||||||
|
* - Linux and Windows: Hunspell
|
||||||
|
*/
|
||||||
|
export class SpellChecker {
|
||||||
|
/**
|
||||||
|
* ctor
|
||||||
|
*
|
||||||
|
* @param {boolean} enabled Whether spell checking is enabled.
|
||||||
|
*/
|
||||||
|
constructor (enabled = true) {
|
||||||
|
// Hunspell is used on Linux and Windows but macOS can use Hunspell if prefered.
|
||||||
|
this.isHunspell = !isOsx || !!process.env['SPELLCHECKER_PREFER_HUNSPELL'] // eslint-disable-line dot-notation
|
||||||
|
|
||||||
|
// Initialize spell check provider. If spell check is not enabled don't
|
||||||
|
// initialize the handler to not load the native module.
|
||||||
|
if (enabled) {
|
||||||
|
this._initHandler()
|
||||||
|
} else {
|
||||||
|
this.provider = null
|
||||||
|
this.fallbackLang = null
|
||||||
|
this.isEnabled = false
|
||||||
|
this.isInitialized = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_initHandler () {
|
||||||
|
if (this.isInitialized) {
|
||||||
|
throw new Error('Invalid state.')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.provider = new SpellCheckHandler()
|
||||||
|
|
||||||
|
// The spell checker is now initialized but not yet enabled. You need to call `init`.
|
||||||
|
this.isEnabled = false
|
||||||
|
this.isInitialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the spell checker and attach it to the window.
|
||||||
|
*
|
||||||
|
* @param {string} lang 4-letter language ISO-code.
|
||||||
|
* @param {boolean} automaticallyIdentifyLanguages Whether we should try to identify the typed language.
|
||||||
|
* @param {boolean} isPassiveMode Should we highlight misspelled words?
|
||||||
|
* @param {[HTMLElement]} container The optional container to attach the automatic spell detection when
|
||||||
|
* using Hunspell. Default `document.body`.
|
||||||
|
* @returns {string} Returns current spell checker language.
|
||||||
|
*/
|
||||||
|
async init (lang = '', automaticallyIdentifyLanguages = false, isPassiveMode = false, container = null) {
|
||||||
|
if (this.isEnabled) {
|
||||||
|
return
|
||||||
|
} else if (!this.isInitialized) {
|
||||||
|
this._initHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lang && !automaticallyIdentifyLanguages) {
|
||||||
|
throw new Error('Init: Either language or automatic language detection must be set.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(spell): Currently not supported by our Hunspell implementation
|
||||||
|
// with a reasonable performance and Node worker threads
|
||||||
|
// doesn't work currently in Electron (Electon#18540).
|
||||||
|
if (this.isHunspell) {
|
||||||
|
automaticallyIdentifyLanguages = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// This just set a variable when using Hunspell and switch the spell checker mode
|
||||||
|
// when using macOS spell checker. Calling switchLanguage after this using macOS
|
||||||
|
// spell checker will deactivate automatic language detection.
|
||||||
|
this.provider.automaticallyIdentifyLanguages =
|
||||||
|
automaticallyIdentifyLanguages || (!this.isHunspell && !lang)
|
||||||
|
|
||||||
|
// If true, don't highlight misspelled words. Just like above, this method only
|
||||||
|
// affect the macOS spell checker.
|
||||||
|
this.provider.isPassiveMode = isPassiveMode
|
||||||
|
|
||||||
|
if (!this.isHunspell && (automaticallyIdentifyLanguages || !lang)) {
|
||||||
|
// Attach the spell checker to the our editor.
|
||||||
|
// NOTE: Calling this method is normally not necessary on macOS with
|
||||||
|
// OS spell checker.
|
||||||
|
this.provider.attachToInput(container)
|
||||||
|
|
||||||
|
this.fallbackLang = null
|
||||||
|
this.isEnabled = true
|
||||||
|
return this.lang
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lang) {
|
||||||
|
// Set to Hunspell fallback language
|
||||||
|
lang = 'en-US'
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to call our switch language method to ensure that the provider is in a valid state.
|
||||||
|
const currentLang = await this._switchLanguage(lang)
|
||||||
|
if (!currentLang) {
|
||||||
|
throw new Error(`Language "${lang}" is not available.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach the spell checker to the our editor.
|
||||||
|
this.provider.attachToInput(container)
|
||||||
|
this.fallbackLang = currentLang
|
||||||
|
this.isEnabled = true
|
||||||
|
return currentLang
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable spell checker.
|
||||||
|
*
|
||||||
|
* NOTE: Using `undefined` will use the existing values.
|
||||||
|
* NOTE: When spell checker is already enabled this method has no effect.
|
||||||
|
*
|
||||||
|
* @param {[string]} lang 4-letter language ISO-code.
|
||||||
|
* @param {[boolean]} automaticallyIdentifyLanguages Whether we should try to identify the typed language.
|
||||||
|
* @param {[boolean]} isPassiveMode Should we highlight misspelled words?
|
||||||
|
*/
|
||||||
|
async enableSpellchecker (lang = undefined, automaticallyIdentifyLanguages = undefined, isPassiveMode = undefined) {
|
||||||
|
if (this.isEnabled) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.provider.enableSpellchecker(
|
||||||
|
lang,
|
||||||
|
automaticallyIdentifyLanguages,
|
||||||
|
isPassiveMode
|
||||||
|
)
|
||||||
|
if (!result) {
|
||||||
|
// Spell checker may be in an invalid state and don't try to recover.
|
||||||
|
this.disableSpellchecker()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fallbackLang = this.lang
|
||||||
|
this.isEnabled = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable spell checker.
|
||||||
|
*/
|
||||||
|
disableSpellchecker () {
|
||||||
|
if (!this.isEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.provider.disableSpellchecker()
|
||||||
|
this.isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a word to the user dictionary.
|
||||||
|
*
|
||||||
|
* @param {string} word The word to add.
|
||||||
|
*/
|
||||||
|
async addToDictionary (word) {
|
||||||
|
return await this.provider.addToDictionary(word)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a word frome the user dictionary.
|
||||||
|
*
|
||||||
|
* @param {string} word The word to remove.
|
||||||
|
*/
|
||||||
|
async removeFromDictionary (word) {
|
||||||
|
return await this.provider.removeFromDictionary(word)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ignore a word for the current runtime.
|
||||||
|
*
|
||||||
|
* @param {string} word The word to ignore.
|
||||||
|
*/
|
||||||
|
ignoreWord (word) {
|
||||||
|
this.provider.ignoreWord(word)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of available dictionaries.
|
||||||
|
* @returns {string[]} Available dictionary languages.
|
||||||
|
*/
|
||||||
|
getAvailableDictionaries () {
|
||||||
|
// NOTE: We only receive the dictionaries when the spellchecker is active
|
||||||
|
// on macOS! Therefore be consistent.
|
||||||
|
if (!this.provider.currentSpellchecker) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isHunspell) {
|
||||||
|
// NB: OS X will return lists that are half just a language, half
|
||||||
|
// language + locale, like ['en', 'pt_BR', 'ko']
|
||||||
|
return this.provider.currentSpellchecker.getAvailableDictionaries()
|
||||||
|
.map(x => {
|
||||||
|
if (x.length === 2) return fallbackLocales[x]
|
||||||
|
try {
|
||||||
|
return normalizeLanguageCode(x)
|
||||||
|
} catch (_) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load hunspell dictionaries from disk.
|
||||||
|
return getAvailableHunspellDictionaries()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the spellchecker trying to detect the typed language automatically?
|
||||||
|
*/
|
||||||
|
get automaticallyIdentifyLanguages () {
|
||||||
|
if (!this.isEnabled) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return this.provider.automaticallyIdentifyLanguages
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the spellchecker trying to detect the typed language automatically?
|
||||||
|
*/
|
||||||
|
set automaticallyIdentifyLanguages (value) {
|
||||||
|
if (!this.isEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(spell): Currently not supported by our Hunspell implementation
|
||||||
|
// with a reasonable performance and Node worker threads
|
||||||
|
// doesn't work currently in Electron (Electon#18540).
|
||||||
|
if (this.isHunspell) {
|
||||||
|
value = false
|
||||||
|
}
|
||||||
|
this.provider.automaticallyIdentifyLanguages = !!value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if not misspelled words should be highlighted.
|
||||||
|
*/
|
||||||
|
get spellcheckerNoUnderline () {
|
||||||
|
if (!this.isEnabled) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return this.provider.spellcheckerNoUnderline
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should we highlight misspelled words.
|
||||||
|
*/
|
||||||
|
set spellcheckerNoUnderline (value) {
|
||||||
|
if (!this.isEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.provider.spellcheckerNoUnderline = !!value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current language.
|
||||||
|
*/
|
||||||
|
get lang () {
|
||||||
|
if (!this.provider) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return this.provider.currentSpellcheckerLanguage
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the spell checker is in an invalid state and therefore deactivated.
|
||||||
|
*/
|
||||||
|
get isInvalidState () {
|
||||||
|
if (!this.provider) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return this.provider.invalidState
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explicitly switch the language to a specific language.
|
||||||
|
*
|
||||||
|
* NOTE: This function can throw an exception.
|
||||||
|
*
|
||||||
|
* @param {string} lang The language code
|
||||||
|
* @returns {string|null} Return the language on success or null.
|
||||||
|
*/
|
||||||
|
async switchLanguage (lang) {
|
||||||
|
if (!this.isEnabled) {
|
||||||
|
throw new Error('Invalid state: spell checker is disabled.')
|
||||||
|
} else if (!lang) {
|
||||||
|
throw new Error('Invalid language.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLang = await this._switchLanguage(lang)
|
||||||
|
if (currentLang) {
|
||||||
|
this.fallbackLang = currentLang
|
||||||
|
}
|
||||||
|
return currentLang
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the given word misspelled.
|
||||||
|
*
|
||||||
|
* @param {string} word The word to check.
|
||||||
|
*/
|
||||||
|
isMisspelled (word) {
|
||||||
|
if (!this.isEnabled) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return this.provider.isMisspelled(word)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get corrections.
|
||||||
|
*
|
||||||
|
* @param {string} word The word to get suggestion for.
|
||||||
|
* @returns {string[]} A array of suggestions.
|
||||||
|
*/
|
||||||
|
async getWordSuggestion (word) {
|
||||||
|
if (!this.isMisspelled(word)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return await this.provider.getCorrectionsForMisspelling(word)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the word at the given offset from the text.
|
||||||
|
*
|
||||||
|
* @param {string} text Text
|
||||||
|
* @param {number} offset Normalized cursor offset (e.g. ab<cursor>c def --> 2)
|
||||||
|
*/
|
||||||
|
static extractWord (text, offset) {
|
||||||
|
if (!text || text.length === 0) {
|
||||||
|
return null
|
||||||
|
} else if (offset < 0) {
|
||||||
|
offset = 0
|
||||||
|
} else if (offset >= text.length) {
|
||||||
|
offset = text.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for the words beginning and end.
|
||||||
|
const left = text.slice(0, offset + 1).search(WORD_DEFINITION)
|
||||||
|
const right = text.slice(offset).search(WORD_SEPARATORS)
|
||||||
|
|
||||||
|
// Cursor is between two word separators (e.g "*<cursor>*" or " <cursor>*")
|
||||||
|
if (left <= -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// The last word in the string is a special case.
|
||||||
|
if (right < 0) {
|
||||||
|
return {
|
||||||
|
left,
|
||||||
|
right: text.length,
|
||||||
|
word: text.slice(left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
left,
|
||||||
|
right: right + offset,
|
||||||
|
word: text.slice(left, right + offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {string} lang The language code
|
||||||
|
* @returns {string|null} Return the language on success or null.
|
||||||
|
*/
|
||||||
|
async _switchLanguage (lang) {
|
||||||
|
const result = await this.provider.switchLanguage(lang)
|
||||||
|
if (!result) {
|
||||||
|
return await this._tryRecover()
|
||||||
|
}
|
||||||
|
return this.lang
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to recover the spell checker's invalid state.
|
||||||
|
*
|
||||||
|
* @returns {string|null} Return the language on success or null.
|
||||||
|
*/
|
||||||
|
async _tryRecover () {
|
||||||
|
const lang = this.fallbackLang
|
||||||
|
if (lang) {
|
||||||
|
// Prevent rekursiv loop.
|
||||||
|
this.fallbackLang = null
|
||||||
|
|
||||||
|
// Try fallback language.
|
||||||
|
const result = await this._switchLanguage(lang)
|
||||||
|
if (result) {
|
||||||
|
this.fallbackLang = lang
|
||||||
|
return lang
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spell checker is deactivated from rekursiv call.
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spell checker is in an invalid state. We can recover it by enabling
|
||||||
|
// with a valid language.
|
||||||
|
this.disableSpellchecker()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
172
src/renderer/spellchecker/languageMap.js
Normal file
172
src/renderer/spellchecker/languageMap.js
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import langMap from 'iso-639-1'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the native language name by language code.
|
||||||
|
*
|
||||||
|
* @param {string} langCode The ISO two or four-letter language code (e.g. en, en-US).
|
||||||
|
*/
|
||||||
|
export const getLanguageName = languageCode => {
|
||||||
|
if (!languageCode || (languageCode.length !== 2 && languageCode.length !== 5)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let language = ''
|
||||||
|
|
||||||
|
// First try to get an exact language via 4-letter ISO code.
|
||||||
|
if (languageCode.length === 5) {
|
||||||
|
language = getHunspellLanguageName(languageCode)
|
||||||
|
if (language) {
|
||||||
|
return language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
language = langMap.getNativeName(languageCode.substr(0, 2))
|
||||||
|
if (language) {
|
||||||
|
// Add language code to distinguish between native name (en-US, en-GB, ...).
|
||||||
|
return `${language} (${languageCode})`
|
||||||
|
}
|
||||||
|
return `Unknown (${languageCode})`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the native language name by language code for supported Hunspell languages.
|
||||||
|
*
|
||||||
|
* @param {string} langCode The ISO 4-letter language code.
|
||||||
|
*/
|
||||||
|
export const getHunspellLanguageName = langCode => {
|
||||||
|
const item = HUNSPELL_DICTIONARY_LANGUAGE_MAP.find(item => item.value === langCode)
|
||||||
|
if (!item) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return item.label
|
||||||
|
}
|
||||||
|
|
||||||
|
// All available Hunspell dictionary languages.
|
||||||
|
// NOTE: Listed as value/label due to settings requirements.
|
||||||
|
export const HUNSPELL_DICTIONARY_LANGUAGE_MAP = [{
|
||||||
|
label: 'Afrikaans', // Afrikaans
|
||||||
|
value: 'af-ZA'
|
||||||
|
}, {
|
||||||
|
label: 'български език', // Bulgarian
|
||||||
|
value: 'bg-BG'
|
||||||
|
}, {
|
||||||
|
label: 'Català', // Catalan
|
||||||
|
value: 'ca-ES'
|
||||||
|
}, {
|
||||||
|
label: 'Česky', // Czech
|
||||||
|
value: 'cs-CZ'
|
||||||
|
}, {
|
||||||
|
label: 'Dansk', // Danish
|
||||||
|
value: 'da-DK'
|
||||||
|
}, {
|
||||||
|
label: 'Deutsch', // German
|
||||||
|
value: 'de-DE'
|
||||||
|
}, {
|
||||||
|
label: 'Ελληνικά', // Greek
|
||||||
|
value: 'el-GR'
|
||||||
|
}, {
|
||||||
|
label: 'English (en-AU)', // English
|
||||||
|
value: 'en-AU'
|
||||||
|
}, {
|
||||||
|
label: 'English (en-CA)', // English
|
||||||
|
value: 'en-CA'
|
||||||
|
}, {
|
||||||
|
label: 'English (en-GB)', // English
|
||||||
|
value: 'en-GB'
|
||||||
|
}, {
|
||||||
|
label: 'English (en-US)', // English
|
||||||
|
value: 'en-US'
|
||||||
|
}, {
|
||||||
|
label: 'Español', // Spanish
|
||||||
|
value: 'es-ES'
|
||||||
|
}, {
|
||||||
|
label: 'Eesti', // Estonian
|
||||||
|
value: 'et-EE'
|
||||||
|
}, {
|
||||||
|
label: 'Føroyskt', // Faroese
|
||||||
|
value: 'fo-FO'
|
||||||
|
}, {
|
||||||
|
label: 'Français', // French
|
||||||
|
value: 'fr-FR'
|
||||||
|
}, {
|
||||||
|
label: 'עברית', // Hebrew (modern)
|
||||||
|
value: 'he-IL'
|
||||||
|
}, {
|
||||||
|
label: 'हिन्दी', // Hindi
|
||||||
|
value: 'hi-IN'
|
||||||
|
}, {
|
||||||
|
label: 'Hhrvatski', // Croatian
|
||||||
|
value: 'hr-HR'
|
||||||
|
}, {
|
||||||
|
label: 'Magyar', // Hungarian
|
||||||
|
value: 'hu-HU'
|
||||||
|
}, {
|
||||||
|
label: 'Bahasa Indonesia', // Indonesian
|
||||||
|
value: 'id-ID'
|
||||||
|
}, {
|
||||||
|
label: 'Italiano', // Italian
|
||||||
|
value: 'it-IT'
|
||||||
|
}, {
|
||||||
|
label: '한국어', // Korean
|
||||||
|
value: 'ko'
|
||||||
|
}, {
|
||||||
|
label: 'Lietuvių', // Lithuanian
|
||||||
|
value: 'lt-LT'
|
||||||
|
}, {
|
||||||
|
label: 'Latviešu', // Latvian
|
||||||
|
value: 'lv-LV'
|
||||||
|
}, {
|
||||||
|
label: 'Norsk', // Norwegian
|
||||||
|
value: 'nb-NO'
|
||||||
|
}, {
|
||||||
|
label: 'Nederlands', // Dutch
|
||||||
|
value: 'nl-NL'
|
||||||
|
}, {
|
||||||
|
label: 'Polski', // Polish
|
||||||
|
value: 'pl-PL'
|
||||||
|
}, {
|
||||||
|
label: 'Português (pt-BR)', // Portuguese
|
||||||
|
value: 'pt-BR'
|
||||||
|
}, {
|
||||||
|
label: 'Português (pt-PT)', // Portuguese
|
||||||
|
value: 'pt-PT'
|
||||||
|
}, {
|
||||||
|
label: 'Română', // Romanian
|
||||||
|
value: 'ro-RO'
|
||||||
|
}, {
|
||||||
|
label: 'Pусский', // Russian
|
||||||
|
value: 'ru-RU'
|
||||||
|
}, {
|
||||||
|
label: 'Cрпски језик (Latin)', // Serbian (Latin)
|
||||||
|
value: 'sh' // aka sr-Latn
|
||||||
|
}, {
|
||||||
|
label: 'Slovenčina', // Slovak
|
||||||
|
value: 'sk-SK'
|
||||||
|
}, {
|
||||||
|
label: 'Slovenščina', // Slovene
|
||||||
|
value: 'sl-SI'
|
||||||
|
}, {
|
||||||
|
label: 'Shqip', // Albanian
|
||||||
|
value: 'sq'
|
||||||
|
}, {
|
||||||
|
label: 'Cрпски језик', // Serbian
|
||||||
|
value: 'sr'
|
||||||
|
}, {
|
||||||
|
label: 'Svenska', // Swedish
|
||||||
|
value: 'sv-SE'
|
||||||
|
}, {
|
||||||
|
label: 'தமிழ்', // Tamil
|
||||||
|
value: 'ta-IN'
|
||||||
|
}, {
|
||||||
|
label: 'тоҷикӣ', // Tajik
|
||||||
|
value: 'tg-TG'
|
||||||
|
}, {
|
||||||
|
label: 'Türkçe', // Turkish
|
||||||
|
value: 'tr-TR'
|
||||||
|
}, {
|
||||||
|
label: 'українська', // Ukrainian
|
||||||
|
value: 'uk-UA'
|
||||||
|
}, {
|
||||||
|
label: 'Tiếng Việt', // Vietnamese
|
||||||
|
value: 'vi-VN'
|
||||||
|
}]
|
@ -42,6 +42,12 @@ const state = {
|
|||||||
|
|
||||||
theme: 'light',
|
theme: 'light',
|
||||||
|
|
||||||
|
spellcheckerEnabled: false,
|
||||||
|
spellcheckerIsHunspell: false, // macOS only
|
||||||
|
spellcheckerNoUnderline: false,
|
||||||
|
spellcheckerAutoDetectLanguage: false,
|
||||||
|
spellcheckerLanguage: 'en-US',
|
||||||
|
|
||||||
// Default values that are overwritten with the entries below.
|
// Default values that are overwritten with the entries below.
|
||||||
sideBarVisibility: false,
|
sideBarVisibility: false,
|
||||||
tabBarVisibility: false,
|
tabBarVisibility: false,
|
||||||
|
@ -39,6 +39,12 @@
|
|||||||
|
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
|
|
||||||
|
"spellcheckerEnabled": false,
|
||||||
|
"spellcheckerIsHunspell": false,
|
||||||
|
"spellcheckerNoUnderline": false,
|
||||||
|
"spellcheckerAutoDetectLanguage": false,
|
||||||
|
"spellcheckerLanguage": "en-US",
|
||||||
|
|
||||||
"sideBarVisibility": false,
|
"sideBarVisibility": false,
|
||||||
"tabBarVisibility": false,
|
"tabBarVisibility": false,
|
||||||
"sourceCodeModeEnabled": false,
|
"sourceCodeModeEnabled": false,
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const path = require('path')
|
|
||||||
const merge = require('webpack-merge')
|
const merge = require('webpack-merge')
|
||||||
const webpack = require('webpack')
|
const webpack = require('webpack')
|
||||||
|
|
||||||
const baseConfig = require('../../.electron-vue/webpack.renderer.config')
|
const baseConfig = require('../../.electron-vue/webpack.renderer.config')
|
||||||
const projectRoot = path.resolve(__dirname, '../../src/renderer')
|
|
||||||
|
|
||||||
// Set BABEL_ENV to use proper preset config
|
// Set BABEL_ENV to use proper preset config
|
||||||
process.env.BABEL_ENV = 'test'
|
process.env.BABEL_ENV = 'test'
|
||||||
|
@ -5,12 +5,22 @@ const fs = require('fs')
|
|||||||
const thirdPartyChecker = require('../.electron-vue/thirdPartyChecker.js')
|
const thirdPartyChecker = require('../.electron-vue/thirdPartyChecker.js')
|
||||||
const rootDir = path.resolve(__dirname, '..')
|
const rootDir = path.resolve(__dirname, '..')
|
||||||
|
|
||||||
|
const additionalPackages = {
|
||||||
|
hunspell: {
|
||||||
|
packageName: 'Hunspell',
|
||||||
|
licenses: 'LGPL 2.1',
|
||||||
|
licenseText: fs.readFileSync(path.join(rootDir, 'resources/hunspell_dictionaries/LICENSE-hunspell.txt'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
thirdPartyChecker.getLicenses(rootDir, (err, packages, checker) => {
|
thirdPartyChecker.getLicenses(rootDir, (err, packages, checker) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(`[ERROR] ${err}`)
|
console.log(`[ERROR] ${err}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object.assign(packages, additionalPackages)
|
||||||
|
|
||||||
let summary = ''
|
let summary = ''
|
||||||
let licenseList = ''
|
let licenseList = ''
|
||||||
let index = 1
|
let index = 1
|
||||||
|
54
yarn.lock
54
yarn.lock
@ -852,6 +852,13 @@
|
|||||||
ajv "^6.1.0"
|
ajv "^6.1.0"
|
||||||
ajv-keywords "^3.1.0"
|
ajv-keywords "^3.1.0"
|
||||||
|
|
||||||
|
"@felixrieseberg/spellchecker@^4.0.10":
|
||||||
|
version "4.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@felixrieseberg/spellchecker/-/spellchecker-4.0.10.tgz#ec4b11bcaa98a45d0f1c768a2f3dfb2b8768ed3f"
|
||||||
|
integrity sha512-b+BlHcBXjx+W7yGNAtoVpAv8dvmAQ8Tp2YhNjqxIgocb6Wq1nKLl4jfu9DG60UWC0hTNvvQ74ny9ojiUFNqGSA==
|
||||||
|
dependencies:
|
||||||
|
nan "^2.13.2"
|
||||||
|
|
||||||
"@hfelix/electron-localshortcut@^3.1.1":
|
"@hfelix/electron-localshortcut@^3.1.1":
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.npmjs.org/@hfelix/electron-localshortcut/-/electron-localshortcut-3.1.1.tgz#97e2f200568bffd5a49b465830b84f76fbc1130d"
|
resolved "https://registry.npmjs.org/@hfelix/electron-localshortcut/-/electron-localshortcut-3.1.1.tgz#97e2f200568bffd5a49b465830b84f76fbc1130d"
|
||||||
@ -862,6 +869,20 @@
|
|||||||
electron-is-accelerator "^0.1.0"
|
electron-is-accelerator "^0.1.0"
|
||||||
keyboardevents-areequal "^0.2.1"
|
keyboardevents-areequal "^0.2.1"
|
||||||
|
|
||||||
|
"@hfelix/electron-spellchecker@^1.0.0-rc.1":
|
||||||
|
version "1.0.0-rc.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hfelix/electron-spellchecker/-/electron-spellchecker-1.0.0-rc.1.tgz#595762825bd77d1dc43cbc38e725e84e5baa989a"
|
||||||
|
integrity sha512-blQCgk2Dw0mep0eQPZzcMG5bYEmf+Zvih48doiGloclgYikxBb8kYR64hKZQycVg+SeXuLeTodL7L2KI5MXn0Q==
|
||||||
|
dependencies:
|
||||||
|
"@felixrieseberg/spellchecker" "^4.0.10"
|
||||||
|
bcp47 "^1.1.2"
|
||||||
|
cld "^2.5.1"
|
||||||
|
debug "^4.1.1"
|
||||||
|
fs-extra "^8.1.0"
|
||||||
|
keyboard-layout "^2.0.16"
|
||||||
|
lru-cache "^5.1.1"
|
||||||
|
p-throttle "^3.1.0"
|
||||||
|
|
||||||
"@hfelix/keyboardevent-from-electron-accelerator@^1.1.1":
|
"@hfelix/keyboardevent-from-electron-accelerator@^1.1.1":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.npmjs.org/@hfelix/keyboardevent-from-electron-accelerator/-/keyboardevent-from-electron-accelerator-1.1.1.tgz#7e1d4fd913759c381b7919cc7faf4c0c641d457c"
|
resolved "https://registry.npmjs.org/@hfelix/keyboardevent-from-electron-accelerator/-/keyboardevent-from-electron-accelerator-1.1.1.tgz#7e1d4fd913759c381b7919cc7faf4c0c641d457c"
|
||||||
@ -1800,6 +1821,11 @@ batch@0.6.1:
|
|||||||
resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
|
resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
|
||||||
integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=
|
integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=
|
||||||
|
|
||||||
|
bcp47@^1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/bcp47/-/bcp47-1.1.2.tgz#354be3307ffd08433a78f5e1e2095845f89fc7fe"
|
||||||
|
integrity sha1-NUvjMH/9CEM6ePXh4glYRfifx/4=
|
||||||
|
|
||||||
bcrypt-pbkdf@^1.0.0:
|
bcrypt-pbkdf@^1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
|
resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
|
||||||
@ -2526,6 +2552,16 @@ class-utils@^0.3.5:
|
|||||||
isobject "^3.0.0"
|
isobject "^3.0.0"
|
||||||
static-extend "^0.1.1"
|
static-extend "^0.1.1"
|
||||||
|
|
||||||
|
cld@^2.5.1:
|
||||||
|
version "2.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/cld/-/cld-2.5.1.tgz#3a0fa74b48fb0adcf280c0975e3ac906a4f5b904"
|
||||||
|
integrity sha512-DwdvvcFVizwDdPCocoPPReFk3BwLEaTZ3RzFgJ4jLzsBzJKUC3cTna0ZmAZG4tFtMmQdl0ciso3+ijkH3OPZPA==
|
||||||
|
dependencies:
|
||||||
|
glob "^5.0.10"
|
||||||
|
nan "^2.9.2"
|
||||||
|
rimraf "^2.4.0"
|
||||||
|
underscore "^1.6.0"
|
||||||
|
|
||||||
clean-css@3.4.x:
|
clean-css@3.4.x:
|
||||||
version "3.4.28"
|
version "3.4.28"
|
||||||
resolved "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz#bf1945e82fc808f55695e6ddeaec01400efd03ff"
|
resolved "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz#bf1945e82fc808f55695e6ddeaec01400efd03ff"
|
||||||
@ -5534,7 +5570,7 @@ glob@7.1.3:
|
|||||||
once "^1.3.0"
|
once "^1.3.0"
|
||||||
path-is-absolute "^1.0.0"
|
path-is-absolute "^1.0.0"
|
||||||
|
|
||||||
glob@^5.0.15:
|
glob@^5.0.10, glob@^5.0.15:
|
||||||
version "5.0.15"
|
version "5.0.15"
|
||||||
resolved "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
|
resolved "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
|
||||||
integrity sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=
|
integrity sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=
|
||||||
@ -6681,6 +6717,11 @@ isexe@^2.0.0:
|
|||||||
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||||
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
|
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
|
||||||
|
|
||||||
|
iso-639-1@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/iso-639-1/-/iso-639-1-2.1.0.tgz#df88d7dd14b39c4dc748f8b35b6c7ae490e9d543"
|
||||||
|
integrity sha512-8CTinLimb9ncAJ11wpCETWZ51qsQ3LS4vMHF2wxRRtR3+b7bvIxUlXOGYIdq0413+baWnbyG5dBluVcezOG/LQ==
|
||||||
|
|
||||||
isobject@^2.0.0, isobject@^2.1.0:
|
isobject@^2.0.0, isobject@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
|
resolved "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
|
||||||
@ -8024,7 +8065,7 @@ mute-stream@0.0.7:
|
|||||||
resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
||||||
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
|
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
|
||||||
|
|
||||||
nan@2.14.0, nan@^2.12.1, nan@^2.13.2:
|
nan@2.14.0, nan@^2.12.1, nan@^2.13.2, nan@^2.9.2:
|
||||||
version "2.14.0"
|
version "2.14.0"
|
||||||
resolved "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
|
resolved "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
|
||||||
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
|
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
|
||||||
@ -8632,6 +8673,11 @@ p-retry@^3.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
retry "^0.12.0"
|
retry "^0.12.0"
|
||||||
|
|
||||||
|
p-throttle@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-throttle/-/p-throttle-3.1.0.tgz#dee34ce4e77d7cc2dfdc1fea0daedccc64147214"
|
||||||
|
integrity sha512-rLo81NXBihs3GJQhq89IXa0Egj/sbW1zW8/qnyadOwUhIUrZSUvyGdQ46ISRKELFBkVvmMJ4JUqWki4oAh30Qw==
|
||||||
|
|
||||||
p-try@^1.0.0:
|
p-try@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
|
resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
|
||||||
@ -10149,7 +10195,7 @@ right-align@^0.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
align-text "^0.1.1"
|
align-text "^0.1.1"
|
||||||
|
|
||||||
rimraf@2, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.3:
|
rimraf@2, rimraf@^2.4.0, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.3:
|
||||||
version "2.7.1"
|
version "2.7.1"
|
||||||
resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||||
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||||
@ -11606,7 +11652,7 @@ ultron@~1.1.0:
|
|||||||
resolved "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
|
resolved "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
|
||||||
integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==
|
integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==
|
||||||
|
|
||||||
underscore@^1.9.1:
|
underscore@^1.6.0, underscore@^1.9.1:
|
||||||
version "1.9.1"
|
version "1.9.1"
|
||||||
resolved "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961"
|
resolved "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961"
|
||||||
integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==
|
integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==
|
||||||
|
Loading…
Reference in New Issue
Block a user