mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 02:09:40 +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,
|
||||
excludePackages: 'xmldom@0.1.27', // xmldom@0.1.27 is under MIT License, but license-checker show it's under LGPL License.
|
||||
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: {
|
||||
licenses: '',
|
||||
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/vega-lite/build/vega-lite*.js.map"
|
||||
# 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/keyboard-layout/bin"
|
||||
- "!node_modules/keytar/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:
|
||||
- "LICENSE"
|
||||
- from: "resources/THIRD-PARTY-LICENSES.txt"
|
||||
to: "THIRD-PARTY-LICENSES.txt"
|
||||
extraResources:
|
||||
- from: "resources/hunspell_dictionaries/"
|
||||
to: "hunspell_dictionaries/"
|
||||
filter:
|
||||
- "!**/LICENSE-hunspell.txt"
|
||||
|
||||
fileAssociations:
|
||||
- ext:
|
||||
|
@ -14,7 +14,7 @@
|
||||
"build:bin": "node .electron-vue/build.js && electron-builder --dir",
|
||||
"build:clean": "cross-env BUILD_TARGET=clean 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",
|
||||
"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",
|
||||
@ -28,12 +28,13 @@
|
||||
"preinstall": "node .electron-vue/preinstall.js",
|
||||
"build:muya": "cd src/muya && webpack --progress --colors --config webpack.config.js",
|
||||
"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",
|
||||
"validate-licenses": "node tools/validateLicenses.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hfelix/electron-localshortcut": "^3.1.1",
|
||||
"@hfelix/electron-spellchecker": "^1.0.0-rc.1",
|
||||
"@octokit/rest": "^16.33.1",
|
||||
"arg": "^4.1.1",
|
||||
"axios": "^0.19.0",
|
||||
@ -60,6 +61,7 @@
|
||||
"github-markdown-css": "^3.0.1",
|
||||
"html-tags": "^3.0.0",
|
||||
"iconv-lite": "^0.5.0",
|
||||
"iso-639-1": "^2.1.0",
|
||||
"joplin-turndown-plugin-gfm": "^1.0.11",
|
||||
"katex": "^0.11.1",
|
||||
"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.
@ -36,7 +36,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
|
||||
-webkit-animation: sk-bounce 2.0s infinite ease-in-out;
|
||||
animation: sk-bounce 2.0s infinite ease-in-out;
|
||||
}
|
||||
@ -52,10 +52,10 @@
|
||||
}
|
||||
|
||||
@keyframes sk-bounce {
|
||||
0%, 100% {
|
||||
0%, 100% {
|
||||
transform: scale(0.0);
|
||||
-webkit-transform: scale(0.0);
|
||||
} 50% {
|
||||
} 50% {
|
||||
transform: scale(1.0);
|
||||
-webkit-transform: scale(1.0);
|
||||
}
|
||||
@ -68,7 +68,7 @@
|
||||
</script>
|
||||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
<body autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
||||
<!-- Vue app -->
|
||||
<div id="app"></div>
|
||||
|
||||
|
@ -10,6 +10,7 @@ import parseArgs from '../cli/parser'
|
||||
import { normalizeMarkdownPath } from '../filesystem/markdown'
|
||||
import { selectTheme } from '../menu/actions/theme'
|
||||
import { dockMenu } from '../menu/templates'
|
||||
import ensureDefaultDict from '../preferences/hunspell'
|
||||
import { watchers } from '../utils/imagePathAutoComplement'
|
||||
import { WindowType } from '../windows/base'
|
||||
import EditorWindow from '../windows/editor'
|
||||
@ -109,6 +110,13 @@ class App {
|
||||
event.preventDefault()
|
||||
})
|
||||
})
|
||||
|
||||
// Copy default (en-US) Hunspell dictionary.
|
||||
const { paths } = this._accessor
|
||||
ensureDefaultDict(paths.userDataPath)
|
||||
.catch(error => {
|
||||
log.error(error)
|
||||
})
|
||||
}
|
||||
|
||||
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"
|
||||
},
|
||||
|
||||
"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": {
|
||||
"description": "Image--The default behavior after insert image from local folder",
|
||||
"enum": [
|
||||
|
@ -400,6 +400,16 @@ class EditorWindow extends BaseWindow {
|
||||
|
||||
// --- 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.
|
||||
*
|
||||
|
@ -255,6 +255,10 @@ export const MUYA_DEFAULT_OPTION = {
|
||||
mermaidTheme: 'default', // dark / forest / default
|
||||
vegaTheme: 'latimes', // excel / ggplot2 / quartz / vox / fivethirtyeight / dark / latimes
|
||||
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
|
||||
imageAction: null,
|
||||
// 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 tableDragBarCtrl from './tableDragBarCtrl'
|
||||
import tableSelectCellsCtrl from './tableSelectCellsCtrl'
|
||||
import coreApi from './core'
|
||||
import History from './history'
|
||||
import arrowCtrl from './arrowCtrl'
|
||||
import pasteCtrl from './pasteCtrl'
|
||||
@ -32,6 +33,7 @@ import Cursor from '../selection/cursor'
|
||||
import escapeCharactersMap, { escapeCharacters } from '../parser/escapeCharacter'
|
||||
|
||||
const prototypes = [
|
||||
coreApi,
|
||||
tabCtrl,
|
||||
enterCtrl,
|
||||
updateCtrl,
|
||||
|
@ -28,9 +28,9 @@ class ClickEvent {
|
||||
if (!start || !end) {
|
||||
return
|
||||
}
|
||||
|
||||
const startBlock = contentState.getBlock(start.key)
|
||||
const nextTextBlock = contentState.findNextBlockInLocation(startBlock)
|
||||
|
||||
if (
|
||||
nextTextBlock && nextTextBlock.key === end.key &&
|
||||
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) {
|
||||
this.contentState.turndownConfig.bulletListMarker = options.bulletListMarker
|
||||
}
|
||||
@ -380,6 +386,21 @@ class Muya {
|
||||
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 () {
|
||||
this.contentState.clear()
|
||||
this.quickInsert.destroy()
|
||||
@ -395,7 +416,7 @@ class Muya {
|
||||
* [ensureContainerDiv ensure container element is div]
|
||||
*/
|
||||
function getContainer (originContainer, options) {
|
||||
const { hideQuickInsertHint } = options
|
||||
const { hideQuickInsertHint, spellcheckEnabled } = options
|
||||
const container = document.createElement('div')
|
||||
const rootDom = document.createElement('div')
|
||||
const attrs = originContainer.attributes
|
||||
@ -411,7 +432,9 @@ function getContainer (originContainer, options) {
|
||||
container.setAttribute('contenteditable', true)
|
||||
container.setAttribute('autocorrect', false)
|
||||
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)
|
||||
originContainer.replaceWith(container)
|
||||
return container
|
||||
|
@ -39,11 +39,15 @@ export default function renderContainerBlock (parent, block, activeBlocks, match
|
||||
}
|
||||
|
||||
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) {
|
||||
selector += `.language-${lang.replace(/[#.]{1}/g, '')}`
|
||||
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||
}
|
||||
|
||||
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)
|
||||
) {
|
||||
selector += `.${CLASS_OR_ID.AG_CONTAINER_BLOCK}`
|
||||
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||
}
|
||||
} else if (/ul|ol/.test(type) && listType) {
|
||||
selector += `.ag-${listType}-list`
|
||||
@ -143,6 +148,7 @@ export default function renderContainerBlock (parent, block, activeBlocks, match
|
||||
selector += `.ag-${listItemType}-list-item`
|
||||
selector += isLooseListItem ? `.${CLASS_OR_ID.AG_LOOSE_LIST_ITEM}` : `.${CLASS_OR_ID.AG_TIGHT_LIST_ITEM}`
|
||||
} else if (type === 'pre') {
|
||||
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||
Object.assign(data.dataset, { role: functionType })
|
||||
selector += PRE_BLOCK_HASH[block.functionType]
|
||||
|
||||
|
@ -115,6 +115,7 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
|
||||
|
||||
if (editable === false) {
|
||||
Object.assign(data.attrs, {
|
||||
spellcheck: 'false',
|
||||
contenteditable: 'false'
|
||||
})
|
||||
}
|
||||
@ -124,7 +125,10 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
|
||||
switch (functionType) {
|
||||
case 'html': {
|
||||
selector += `.${CLASS_OR_ID.AG_HTML_PREVIEW}`
|
||||
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||
|
||||
const htmlContent = sanitize(code, PREVIEW_DOMPURIFY_CONFIG)
|
||||
|
||||
// handle empty html bock
|
||||
if (/^<([a-z][a-z\d]*)[^>]*?>(\s*)<\/\1>$/.test(htmlContent.trim())) {
|
||||
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': {
|
||||
const key = `${code}_display_math`
|
||||
selector += `.${CLASS_OR_ID.AG_CONTAINER_PREVIEW}`
|
||||
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||
if (code === '') {
|
||||
children = '< Empty Mathematical Formula >'
|
||||
selector += `.${CLASS_OR_ID.AG_EMPTY}`
|
||||
@ -167,6 +172,7 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
|
||||
}
|
||||
case 'mermaid': {
|
||||
selector += `.${CLASS_OR_ID.AG_CONTAINER_PREVIEW}`
|
||||
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||
if (code === '') {
|
||||
children = '< Empty Mermaid Block >'
|
||||
selector += `.${CLASS_OR_ID.AG_EMPTY}`
|
||||
@ -186,6 +192,7 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
|
||||
case 'sequence':
|
||||
case 'vega-lite': {
|
||||
selector += `.${CLASS_OR_ID.AG_CONTAINER_PREVIEW}`
|
||||
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||
if (code === '') {
|
||||
children = '< Empty Diagram Block >'
|
||||
selector += `.${CLASS_OR_ID.AG_EMPTY}`
|
||||
@ -236,6 +243,7 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
|
||||
const html = getHighlightHtml(text, highlights)
|
||||
children = htmlToVNode(html)
|
||||
}
|
||||
|
||||
if (!block.parent) {
|
||||
return h(selector, data, [this.renderIcon(block), ...children])
|
||||
} else {
|
||||
|
@ -13,6 +13,9 @@ export default function autoLink (h, cursor, block, token, outerClass) {
|
||||
return [
|
||||
h(`span.${className}`, startMarker),
|
||||
h(`a.${CLASS_OR_ID.AG_INLINE_RULE}.${CLASS_OR_ID.AG_AUTO_LINK}`, {
|
||||
attrs: {
|
||||
spellcheck: 'false'
|
||||
},
|
||||
props: {
|
||||
href: isLink ? encodeURI(href) : `mailto:${email}`,
|
||||
target: '_blank'
|
||||
|
@ -9,6 +9,9 @@ export default function autoLinkExtension (h, cursor, block, token, outerClass)
|
||||
|
||||
return [
|
||||
h(`a.${CLASS_OR_ID.AG_INLINE_RULE}.${CLASS_OR_ID.AG_AUTO_LINK_EXTENSION}`, {
|
||||
attrs: {
|
||||
spellcheck: 'false'
|
||||
},
|
||||
props: {
|
||||
href: linkType === 'www' ? encodeURI(`http://${www}`) : (linkType === 'url' ? encodeURI(url) : `mailto:${email}`),
|
||||
target: '_blank'
|
||||
|
@ -9,6 +9,10 @@ export default function codeFense (h, cursor, block, token, outerClass) {
|
||||
|
||||
return [
|
||||
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
|
||||
? h(contentSelector, {
|
||||
attrs: {
|
||||
spellcheck: 'false'
|
||||
},
|
||||
dataset: {
|
||||
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.${CLASS_OR_ID.AG_INLINE_RULE}.${CLASS_OR_ID.AG_RUBY_TEXT}`, content),
|
||||
h(previewSelector, {
|
||||
attrs: { contenteditable: 'false' }
|
||||
attrs: {
|
||||
contenteditable: 'false',
|
||||
spellcheck: 'false'
|
||||
}
|
||||
}, vNode)
|
||||
])
|
||||
// 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]
|
||||
}, [])
|
||||
: ''
|
||||
|
||||
switch (tag) {
|
||||
// Handle html img.
|
||||
case 'img': {
|
||||
@ -49,6 +50,12 @@ export default function htmlTag (h, cursor, block, token, outerClass) {
|
||||
raw: token.raw
|
||||
}
|
||||
}
|
||||
|
||||
// Disable spell checking for these tags
|
||||
if (tag === 'code' || tag === 'kbd') {
|
||||
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||
}
|
||||
|
||||
if (attrs.id) {
|
||||
selector += `#${attrs.id}`
|
||||
}
|
||||
@ -66,9 +73,17 @@ export default function htmlTag (h, cursor, block, token, outerClass) {
|
||||
}
|
||||
|
||||
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(`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 [
|
||||
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)
|
||||
]
|
||||
}
|
||||
|
@ -43,7 +43,9 @@ export default function displayMath (h, cursor, block, token, outerClass) {
|
||||
return [
|
||||
h(`span.${className}.${CLASS_OR_ID.AG_MATH_MARKER}`, startMarker),
|
||||
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, {
|
||||
attrs: { contenteditable: 'false' }
|
||||
}, mathVnode)
|
||||
|
@ -70,7 +70,9 @@ export default function link (h, cursor, block, token, outerClass) {
|
||||
...this.backlashInToken(h, token.backlash.first, className, firstBacklashStart, token)
|
||||
]),
|
||||
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,
|
||||
...this.backlashInToken(h, token.backlash.second, className, secondBacklashStart, token)
|
||||
]),
|
||||
|
@ -42,10 +42,22 @@ export default function referenceDefinition (h, cursor, block, token, outerClass
|
||||
|
||||
return [
|
||||
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),
|
||||
h(`span.${className}`, middleContent),
|
||||
h(`span.${className}`, {
|
||||
attrs: {
|
||||
spellcheck: 'false'
|
||||
}
|
||||
}, middleContent),
|
||||
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 data = {
|
||||
attrs: {
|
||||
spellcheck: 'false'
|
||||
},
|
||||
props: {
|
||||
title
|
||||
},
|
||||
|
42
src/renderer/bootstrap.js
vendored
42
src/renderer/bootstrap.js
vendored
@ -27,6 +27,7 @@ const parseUrlArgs = () => {
|
||||
const userDataPath = params.get('udp')
|
||||
const windowId = Number(params.get('wid'))
|
||||
const type = params.get('type')
|
||||
const spellcheckerIsHunspell = params.get('slp') === '1'
|
||||
|
||||
if (Number.isNaN(windowId)) {
|
||||
throw new Error('Error while parsing URL arguments: windowId!')
|
||||
@ -43,7 +44,8 @@ const parseUrlArgs = () => {
|
||||
hideScrollbar,
|
||||
theme,
|
||||
titleBarStyle
|
||||
}
|
||||
},
|
||||
spellcheckerIsHunspell
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,20 +60,31 @@ const bootstrapRenderer = () => {
|
||||
|
||||
// Register renderer exception handler
|
||||
window.addEventListener('error', event => {
|
||||
const { message, name, stack } = event.error
|
||||
const copy = {
|
||||
message,
|
||||
name,
|
||||
stack
|
||||
if (event.error) {
|
||||
const { message, name, stack } = event.error
|
||||
const copy = {
|
||||
message,
|
||||
name,
|
||||
stack
|
||||
}
|
||||
|
||||
exceptionLogger(event.error)
|
||||
|
||||
// Pass exception to main process exception handler to show a error dialog.
|
||||
ipcRenderer.send('AGANI::handle-renderer-error', copy)
|
||||
} else {
|
||||
console.error(event)
|
||||
}
|
||||
|
||||
exceptionLogger(event.error)
|
||||
|
||||
// Pass exception to main process exception handler to show a error dialog.
|
||||
ipcRenderer.send('AGANI::handle-renderer-error', copy)
|
||||
})
|
||||
|
||||
const { debug, initialState, userDataPath, windowId, type } = parseUrlArgs()
|
||||
const {
|
||||
debug,
|
||||
initialState,
|
||||
userDataPath,
|
||||
windowId,
|
||||
type,
|
||||
spellcheckerIsHunspell
|
||||
} = parseUrlArgs()
|
||||
const paths = new RendererPaths(userDataPath)
|
||||
const marktext = {
|
||||
initialState,
|
||||
@ -85,6 +98,11 @@ const bootstrapRenderer = () => {
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -73,6 +73,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { shell } from 'electron'
|
||||
import log from 'electron-log'
|
||||
import { mapState } from 'vuex'
|
||||
import ViewImage from 'view-image'
|
||||
import Muya from 'muya/lib'
|
||||
@ -88,22 +90,22 @@ import FormatPicker from 'muya/lib/ui/formatPicker'
|
||||
import LinkTools from 'muya/lib/ui/linkTools'
|
||||
import TableBarTools from 'muya/lib/ui/tableTools'
|
||||
import FrontMenu from 'muya/lib/ui/frontMenu'
|
||||
import bus from '../../bus'
|
||||
import Search from '../search'
|
||||
import { animatedScrollTo } from '../../util'
|
||||
import { addCommonStyle, setEditorWidth } from '../../util/theme'
|
||||
import { guessClipboardFilePath } from '../../util/clipboard'
|
||||
import { showContextMenu } from '../../contextMenu/editor'
|
||||
import Printer from '@/services/printService'
|
||||
import bus from '@/bus'
|
||||
import { DEFAULT_EDITOR_FONT_FAMILY } from '@/config'
|
||||
import { moveImageToFolder, uploadImage } from '@/util/fileSystem'
|
||||
import { showContextMenu } from '@/contextMenu/editor'
|
||||
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 '@/assets/themes/codemirror/one-dark.css'
|
||||
import 'view-image/lib/imgViewer.css'
|
||||
import CloseIcon from '@/assets/icons/close.svg'
|
||||
import { shell } from 'electron'
|
||||
|
||||
const STANDAR_Y = 320
|
||||
|
||||
@ -147,6 +149,11 @@ export default {
|
||||
imageFolderPath: state => state.preferences.imageFolderPath,
|
||||
theme: state => state.preferences.theme,
|
||||
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,
|
||||
|
||||
@ -159,6 +166,8 @@ export default {
|
||||
data () {
|
||||
this.defaultFontFamily = DEFAULT_EDITOR_FONT_FAMILY
|
||||
this.CloseIcon = CloseIcon
|
||||
// Helper to ignore changes when the spell check provider was changed in settings.
|
||||
this.spellcheckerIgnorChanges = false
|
||||
return {
|
||||
selectionChange: 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) {
|
||||
if (value && value !== oldValue) {
|
||||
this.scrollToCursor(0)
|
||||
@ -342,7 +433,8 @@ export default {
|
||||
frontmatterType,
|
||||
hideQuickInsertHint,
|
||||
editorLineWidth,
|
||||
theme
|
||||
theme,
|
||||
spellcheckerEnabled
|
||||
} = this
|
||||
|
||||
// use muya UI plugins
|
||||
@ -379,6 +471,7 @@ export default {
|
||||
listIndentation,
|
||||
frontmatterType,
|
||||
hideQuickInsertHint,
|
||||
spellcheckEnabled: spellcheckerEnabled,
|
||||
imageAction: this.imageAction.bind(this),
|
||||
imagePathPicker: this.imagePathPicker.bind(this),
|
||||
clipboardFilePath: guessClipboardFilePath,
|
||||
@ -399,6 +492,12 @@ export default {
|
||||
|
||||
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) {
|
||||
this.scrollToCursor()
|
||||
}
|
||||
@ -429,6 +528,7 @@ export default {
|
||||
bus.$on('scroll-to-header', this.scrollToHeader)
|
||||
bus.$on('print', this.handlePrint)
|
||||
bus.$on('screenshot-captured', this.handleScreenShot)
|
||||
bus.$on('switch-spellchecker-language', this.switchSpellcheckLanguage)
|
||||
|
||||
this.editor.on('change', changes => {
|
||||
// WORKAROUND: "id: 'muya'"
|
||||
@ -436,7 +536,6 @@ export default {
|
||||
})
|
||||
|
||||
this.editor.on('format-click', ({ event, formatType, data }) => {
|
||||
const isOsx = this.platform === 'darwin'
|
||||
const ctrlOrMeta = (isOsx && event.metaKey) || (!isOsx && event.ctrlKey)
|
||||
if (formatType === 'link' && ctrlOrMeta) {
|
||||
this.$store.dispatch('FORMAT_LINK_CLICK', { data, dirname: window.DIRNAME })
|
||||
@ -481,8 +580,35 @@ export default {
|
||||
this.$store.dispatch('SELECTION_FORMATS', formats)
|
||||
})
|
||||
|
||||
this.editor.on('contextmenu', (event, selectionChanges) => {
|
||||
showContextMenu(event, selectionChanges)
|
||||
this.editor.on('contextmenu', (event, selection) => {
|
||||
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)
|
||||
@ -496,7 +622,6 @@ export default {
|
||||
},
|
||||
jumpClick: (linkInfo) => {
|
||||
const { href } = linkInfo
|
||||
|
||||
if (href && href.startsWith('http')) {
|
||||
shell.openExternal(href)
|
||||
}
|
||||
@ -564,6 +689,107 @@ export default {
|
||||
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 () {
|
||||
if (this.editor) {
|
||||
this.editor.undo()
|
||||
@ -789,6 +1015,7 @@ export default {
|
||||
bus.$off('scroll-to-header', this.scrollToHeader)
|
||||
bus.$off('print', this.handlePrint)
|
||||
bus.$off('screenshot-captured', this.handleScreenShot)
|
||||
bus.$off('switch-spellchecker-language', this.switchSpellcheckLanguage)
|
||||
|
||||
document.removeEventListener('keyup', this.keyup)
|
||||
|
||||
|
@ -10,16 +10,37 @@ import {
|
||||
INSERT_BEFORE,
|
||||
INSERT_AFTER
|
||||
} from './menuItems'
|
||||
import spellcheckMenuBuilder from './spellcheck'
|
||||
|
||||
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 win = remote.getCurrentWindow()
|
||||
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]
|
||||
|
||||
;[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
|
||||
})
|
||||
|
||||
|
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,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Table,
|
||||
TableColumn,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Input
|
||||
@ -78,6 +80,8 @@ Vue.use(Select)
|
||||
Vue.use(Option)
|
||||
Vue.use(Radio)
|
||||
Vue.use(RadioGroup)
|
||||
Vue.use(Table)
|
||||
Vue.use(TableColumn)
|
||||
Vue.use(Tabs)
|
||||
Vue.use(TabPane)
|
||||
Vue.use(Input)
|
||||
|
@ -22,6 +22,11 @@ export const category = [{
|
||||
label: 'markdown',
|
||||
icon: MarkdownIcon,
|
||||
path: '/preference/markdown'
|
||||
}, {
|
||||
name: 'Spelling',
|
||||
label: 'spelling',
|
||||
icon: GeneralIcon, // TODO: replace icon
|
||||
path: '/preference/spelling'
|
||||
}, {
|
||||
name: '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 Editor from '@/prefComponents/editor'
|
||||
import Markdown from '@/prefComponents/markdown'
|
||||
import SpellChecker from '@/prefComponents/spellchecker'
|
||||
import Theme from '@/prefComponents/theme'
|
||||
import Image from '@/prefComponents/image'
|
||||
import ImageUploader from '@/prefComponents/imageUploader'
|
||||
@ -22,6 +23,8 @@ const routes = type => ([{
|
||||
path: 'editor', component: Editor, name: 'editor'
|
||||
}, {
|
||||
path: 'markdown', component: Markdown, name: 'markdown'
|
||||
}, {
|
||||
path: 'spelling', component: SpellChecker, name: 'spelling'
|
||||
}, {
|
||||
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',
|
||||
|
||||
spellcheckerEnabled: false,
|
||||
spellcheckerIsHunspell: false, // macOS only
|
||||
spellcheckerNoUnderline: false,
|
||||
spellcheckerAutoDetectLanguage: false,
|
||||
spellcheckerLanguage: 'en-US',
|
||||
|
||||
// Default values that are overwritten with the entries below.
|
||||
sideBarVisibility: false,
|
||||
tabBarVisibility: false,
|
||||
|
@ -39,6 +39,12 @@
|
||||
|
||||
"theme": "light",
|
||||
|
||||
"spellcheckerEnabled": false,
|
||||
"spellcheckerIsHunspell": false,
|
||||
"spellcheckerNoUnderline": false,
|
||||
"spellcheckerAutoDetectLanguage": false,
|
||||
"spellcheckerLanguage": "en-US",
|
||||
|
||||
"sideBarVisibility": false,
|
||||
"tabBarVisibility": false,
|
||||
"sourceCodeModeEnabled": false,
|
||||
|
@ -1,11 +1,9 @@
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const merge = require('webpack-merge')
|
||||
const webpack = require('webpack')
|
||||
|
||||
const baseConfig = require('../../.electron-vue/webpack.renderer.config')
|
||||
const projectRoot = path.resolve(__dirname, '../../src/renderer')
|
||||
|
||||
// Set BABEL_ENV to use proper preset config
|
||||
process.env.BABEL_ENV = 'test'
|
||||
|
@ -5,12 +5,22 @@ const fs = require('fs')
|
||||
const thirdPartyChecker = require('../.electron-vue/thirdPartyChecker.js')
|
||||
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) => {
|
||||
if (err) {
|
||||
console.log(`[ERROR] ${err}`)
|
||||
return
|
||||
}
|
||||
|
||||
Object.assign(packages, additionalPackages)
|
||||
|
||||
let summary = ''
|
||||
let licenseList = ''
|
||||
let index = 1
|
||||
|
54
yarn.lock
54
yarn.lock
@ -852,6 +852,13 @@
|
||||
ajv "^6.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":
|
||||
version "3.1.1"
|
||||
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"
|
||||
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":
|
||||
version "1.1.1"
|
||||
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"
|
||||
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:
|
||||
version "1.0.2"
|
||||
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"
|
||||
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:
|
||||
version "3.4.28"
|
||||
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"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^5.0.15:
|
||||
glob@^5.0.10, glob@^5.0.15:
|
||||
version "5.0.15"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
|
||||
integrity sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=
|
||||
@ -6681,6 +6717,11 @@ isexe@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
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:
|
||||
version "2.1.0"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
|
||||
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
|
||||
@ -8632,6 +8673,11 @@ p-retry@^3.0.1:
|
||||
dependencies:
|
||||
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:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
|
||||
@ -10149,7 +10195,7 @@ right-align@^0.1.1:
|
||||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||
@ -11606,7 +11652,7 @@ ultron@~1.1.0:
|
||||
resolved "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
|
||||
integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==
|
||||
|
||||
underscore@^1.9.1:
|
||||
underscore@^1.6.0, underscore@^1.9.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961"
|
||||
integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==
|
||||
|
Loading…
Reference in New Issue
Block a user