init
This commit is contained in:
commit
f61200f54e
5
.idea/.gitignore
vendored
Normal file
5
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
12
.idea/Markdown2Html.iml
Normal file
12
.idea/Markdown2Html.iml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/Markdown2Html.iml" filepath="$PROJECT_DIR$/.idea/Markdown2Html.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
674
LICENSE
Normal file
674
LICENSE
Normal file
@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, 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
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If 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 convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU 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
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state 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 program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program 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 General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
18
README.md
Normal file
18
README.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<div align="center">
|
||||||
|
<a href="https://md.luckday.cn">
|
||||||
|
<img width="500" src="./screenshot.jpg"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1 align="center">Markdown2Html</h1>
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
Fork 自 [markdown2html](https://github.com/TaleAi/markdown2html),略有调整。
|
||||||
|
|
||||||
|
- 支持自定义样式的 Markdown 编辑器
|
||||||
|
- 支持微信公众号、知乎和稀土掘金
|
||||||
|
- 支持公式
|
||||||
|
- 支持 html 转 markdwon
|
||||||
|
- 支持导出 pdf 和 markdown
|
||||||
|
- 在线使用:
|
||||||
|
- https://md.luckday.cn/
|
93
config/env.js
Normal file
93
config/env.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const paths = require('./paths');
|
||||||
|
|
||||||
|
// Make sure that including paths.js after env.js will read .env variables.
|
||||||
|
delete require.cache[require.resolve('./paths')];
|
||||||
|
|
||||||
|
const NODE_ENV = process.env.NODE_ENV;
|
||||||
|
if (!NODE_ENV) {
|
||||||
|
throw new Error(
|
||||||
|
'The NODE_ENV environment variable is required but was not specified.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
|
||||||
|
var dotenvFiles = [
|
||||||
|
`${paths.dotenv}.${NODE_ENV}.local`,
|
||||||
|
`${paths.dotenv}.${NODE_ENV}`,
|
||||||
|
// Don't include `.env.local` for `test` environment
|
||||||
|
// since normally you expect tests to produce the same
|
||||||
|
// results for everyone
|
||||||
|
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
|
||||||
|
paths.dotenv,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
// Load environment variables from .env* files. Suppress warnings using silent
|
||||||
|
// if this file is missing. dotenv will never modify any environment variables
|
||||||
|
// that have already been set. Variable expansion is supported in .env files.
|
||||||
|
// https://github.com/motdotla/dotenv
|
||||||
|
// https://github.com/motdotla/dotenv-expand
|
||||||
|
dotenvFiles.forEach(dotenvFile => {
|
||||||
|
if (fs.existsSync(dotenvFile)) {
|
||||||
|
require('dotenv-expand')(
|
||||||
|
require('dotenv').config({
|
||||||
|
path: dotenvFile,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// We support resolving modules according to `NODE_PATH`.
|
||||||
|
// This lets you use absolute paths in imports inside large monorepos:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/253.
|
||||||
|
// It works similar to `NODE_PATH` in Node itself:
|
||||||
|
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
|
||||||
|
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
|
||||||
|
// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims.
|
||||||
|
// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
|
||||||
|
// We also resolve them to make sure all tools using them work consistently.
|
||||||
|
const appDirectory = fs.realpathSync(process.cwd());
|
||||||
|
process.env.NODE_PATH = (process.env.NODE_PATH || '')
|
||||||
|
.split(path.delimiter)
|
||||||
|
.filter(folder => folder && !path.isAbsolute(folder))
|
||||||
|
.map(folder => path.resolve(appDirectory, folder))
|
||||||
|
.join(path.delimiter);
|
||||||
|
|
||||||
|
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
|
||||||
|
// injected into the application via DefinePlugin in Webpack configuration.
|
||||||
|
const REACT_APP = /^REACT_APP_/i;
|
||||||
|
|
||||||
|
function getClientEnvironment(publicUrl) {
|
||||||
|
const raw = Object.keys(process.env)
|
||||||
|
.filter(key => REACT_APP.test(key))
|
||||||
|
.reduce(
|
||||||
|
(env, key) => {
|
||||||
|
env[key] = process.env[key];
|
||||||
|
return env;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Useful for determining whether we’re running in production mode.
|
||||||
|
// Most importantly, it switches React into the correct mode.
|
||||||
|
NODE_ENV: process.env.NODE_ENV || 'development',
|
||||||
|
// Useful for resolving the correct path to static assets in `public`.
|
||||||
|
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
|
||||||
|
// This should only be used as an escape hatch. Normally you would put
|
||||||
|
// images into the `src` and `import` them in code to get their paths.
|
||||||
|
PUBLIC_URL: publicUrl,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Stringify all values so we can feed into Webpack DefinePlugin
|
||||||
|
const stringified = {
|
||||||
|
'process.env': Object.keys(raw).reduce((env, key) => {
|
||||||
|
env[key] = JSON.stringify(raw[key]);
|
||||||
|
return env;
|
||||||
|
}, {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { raw, stringified };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getClientEnvironment;
|
14
config/jest/cssTransform.js
Normal file
14
config/jest/cssTransform.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// This is a custom Jest transformer turning style imports into empty objects.
|
||||||
|
// http://facebook.github.io/jest/docs/en/webpack.html
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
process() {
|
||||||
|
return 'module.exports = {};';
|
||||||
|
},
|
||||||
|
getCacheKey() {
|
||||||
|
// The output is always the same.
|
||||||
|
return 'cssTransform';
|
||||||
|
},
|
||||||
|
};
|
30
config/jest/fileTransform.js
Normal file
30
config/jest/fileTransform.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// This is a custom Jest transformer turning file imports into filenames.
|
||||||
|
// http://facebook.github.io/jest/docs/en/webpack.html
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
process(src, filename) {
|
||||||
|
const assetFilename = JSON.stringify(path.basename(filename));
|
||||||
|
|
||||||
|
if (filename.match(/\.svg$/)) {
|
||||||
|
return `module.exports = {
|
||||||
|
__esModule: true,
|
||||||
|
default: ${assetFilename},
|
||||||
|
ReactComponent: (props) => ({
|
||||||
|
$$typeof: Symbol.for('react.element'),
|
||||||
|
type: 'svg',
|
||||||
|
ref: null,
|
||||||
|
key: null,
|
||||||
|
props: Object.assign({}, props, {
|
||||||
|
children: ${assetFilename}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `module.exports = ${assetFilename};`;
|
||||||
|
},
|
||||||
|
};
|
84
config/paths.js
Normal file
84
config/paths.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const url = require("url");
|
||||||
|
|
||||||
|
// Make sure any symlinks in the project folder are resolved:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/637
|
||||||
|
const appDirectory = fs.realpathSync(process.cwd());
|
||||||
|
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);
|
||||||
|
|
||||||
|
const envPublicUrl = process.env.PUBLIC_URL;
|
||||||
|
|
||||||
|
function ensureSlash(inputPath, needsSlash) {
|
||||||
|
const hasSlash = inputPath.endsWith("/");
|
||||||
|
if (hasSlash && !needsSlash) {
|
||||||
|
return inputPath.substr(0, inputPath.length - 1);
|
||||||
|
} else if (!hasSlash && needsSlash) {
|
||||||
|
return `${inputPath}/`;
|
||||||
|
} else {
|
||||||
|
return inputPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPublicUrl = (appPackageJson) => envPublicUrl || require(appPackageJson).homepage;
|
||||||
|
|
||||||
|
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
|
||||||
|
// "public path" at which the app is served.
|
||||||
|
// Webpack needs to know it to put the right <script> hrefs into HTML even in
|
||||||
|
// single-page apps that may serve index.html for nested URLs like /todos/42.
|
||||||
|
// We can't use a relative path in HTML because we don't want to load something
|
||||||
|
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
|
||||||
|
function getServedPath(appPackageJson) {
|
||||||
|
const publicUrl = getPublicUrl(appPackageJson);
|
||||||
|
const servedUrl = envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : "/");
|
||||||
|
return ensureSlash(servedUrl, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleFileExtensions = [
|
||||||
|
"web.mjs",
|
||||||
|
"mjs",
|
||||||
|
"web.js",
|
||||||
|
"js",
|
||||||
|
"web.ts",
|
||||||
|
"ts",
|
||||||
|
"web.tsx",
|
||||||
|
"tsx",
|
||||||
|
"json",
|
||||||
|
"web.jsx",
|
||||||
|
"jsx",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Resolve file paths in the same order as webpack
|
||||||
|
const resolveModule = (resolveFn, filePath) => {
|
||||||
|
const extension = moduleFileExtensions.find((extension) => fs.existsSync(resolveFn(`${filePath}.${extension}`)));
|
||||||
|
|
||||||
|
if (extension) {
|
||||||
|
return resolveFn(`${filePath}.${extension}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveFn(`${filePath}.js`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// config after eject: we're in ./config/
|
||||||
|
module.exports = {
|
||||||
|
dotenv: resolveApp(".env"),
|
||||||
|
appPath: resolveApp("."),
|
||||||
|
appBuild: resolveApp("build"),
|
||||||
|
appPublic: resolveApp("public"),
|
||||||
|
appHtml: resolveApp("public/index.html"),
|
||||||
|
appIndexJs: resolveModule(resolveApp, "src/index"),
|
||||||
|
appLib: resolveModule(resolveApp, "src/Lib"),
|
||||||
|
appPackageJson: resolveApp("package.json"),
|
||||||
|
appSrc: resolveApp("src"),
|
||||||
|
appTsConfig: resolveApp("tsconfig.json"),
|
||||||
|
yarnLockFile: resolveApp("yarn.lock"),
|
||||||
|
testsSetup: resolveModule(resolveApp, "src/setupTests"),
|
||||||
|
proxySetup: resolveApp("src/setupProxy.js"),
|
||||||
|
appNodeModules: resolveApp("node_modules"),
|
||||||
|
publicUrl: getPublicUrl(resolveApp("package.json")),
|
||||||
|
servedPath: getServedPath(resolveApp("package.json")),
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.moduleFileExtensions = moduleFileExtensions;
|
608
config/webpack.config.js
Normal file
608
config/webpack.config.js
Normal file
@ -0,0 +1,608 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const webpack = require("webpack");
|
||||||
|
const resolve = require("resolve");
|
||||||
|
const PnpWebpackPlugin = require("pnp-webpack-plugin");
|
||||||
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
|
const CaseSensitivePathsPlugin = require("case-sensitive-paths-webpack-plugin");
|
||||||
|
const InlineChunkHtmlPlugin = require("react-dev-utils/InlineChunkHtmlPlugin");
|
||||||
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
|
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||||
|
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
|
||||||
|
const safePostCssParser = require("postcss-safe-parser");
|
||||||
|
const ManifestPlugin = require("webpack-manifest-plugin");
|
||||||
|
const InterpolateHtmlPlugin = require("react-dev-utils/InterpolateHtmlPlugin");
|
||||||
|
const WorkboxWebpackPlugin = require("workbox-webpack-plugin");
|
||||||
|
const WatchMissingNodeModulesPlugin = require("react-dev-utils/WatchMissingNodeModulesPlugin");
|
||||||
|
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin");
|
||||||
|
const getCSSModuleLocalIdent = require("react-dev-utils/getCSSModuleLocalIdent");
|
||||||
|
const paths = require("./paths");
|
||||||
|
const getClientEnvironment = require("./env");
|
||||||
|
const ModuleNotFoundPlugin = require("react-dev-utils/ModuleNotFoundPlugin");
|
||||||
|
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin-alt");
|
||||||
|
const typescriptFormatter = require("react-dev-utils/typescriptFormatter");
|
||||||
|
|
||||||
|
// Source maps are resource heavy and can cause out of memory issue for large source files.
|
||||||
|
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false";
|
||||||
|
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
|
||||||
|
// makes for a smoother build process.
|
||||||
|
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== "false";
|
||||||
|
|
||||||
|
// Check if TypeScript is setup
|
||||||
|
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||||
|
|
||||||
|
// style files regexes
|
||||||
|
const cssRegex = /\.css$/;
|
||||||
|
const cssModuleRegex = /\.module\.css$/;
|
||||||
|
const sassRegex = /\.(scss|sass)$/;
|
||||||
|
const sassModuleRegex = /\.module\.(scss|sass)$/;
|
||||||
|
|
||||||
|
// This is the production and development configuration.
|
||||||
|
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
|
||||||
|
module.exports = function(webpackEnv) {
|
||||||
|
const isEnvDevelopment = webpackEnv === "development";
|
||||||
|
const isEnvProduction = webpackEnv === "production";
|
||||||
|
|
||||||
|
// Webpack uses `publicPath` to determine where the app is being served from.
|
||||||
|
// It requires a trailing slash, or the file assets will get an incorrect path.
|
||||||
|
// In development, we always serve from the root. This makes config easier.
|
||||||
|
const publicPath = isEnvProduction ? paths.servedPath : isEnvDevelopment && "/";
|
||||||
|
// Some apps do not use client-side routing with pushState.
|
||||||
|
// For these, "homepage" can be set to "." to enable relative asset paths.
|
||||||
|
const shouldUseRelativeAssetPaths = publicPath === "./";
|
||||||
|
|
||||||
|
// `publicUrl` is just like `publicPath`, but we will provide it to our app
|
||||||
|
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
|
||||||
|
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
|
||||||
|
const publicUrl = isEnvProduction ? publicPath.slice(0, -1) : isEnvDevelopment && "";
|
||||||
|
// Get environment variables to inject into our app.
|
||||||
|
const env = getClientEnvironment(publicUrl);
|
||||||
|
|
||||||
|
// common function to get style loaders
|
||||||
|
const getStyleLoaders = (cssOptions, preProcessor) => {
|
||||||
|
const loaders = [
|
||||||
|
isEnvDevelopment && require.resolve("style-loader"),
|
||||||
|
isEnvProduction && {
|
||||||
|
loader: MiniCssExtractPlugin.loader,
|
||||||
|
options: Object.assign({}, shouldUseRelativeAssetPaths ? {publicPath: "../../"} : undefined),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: require.resolve("css-loader"),
|
||||||
|
options: cssOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Options for PostCSS as we reference these options twice
|
||||||
|
// Adds vendor prefixing based on your specified browser support in
|
||||||
|
// package.json
|
||||||
|
loader: require.resolve("postcss-loader"),
|
||||||
|
options: {
|
||||||
|
// Necessary for external CSS imports to work
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2677
|
||||||
|
ident: "postcss",
|
||||||
|
plugins: () => [
|
||||||
|
require("postcss-flexbugs-fixes"),
|
||||||
|
require("postcss-preset-env")({
|
||||||
|
autoprefixer: {
|
||||||
|
flexbox: "no-2009",
|
||||||
|
},
|
||||||
|
stage: 3,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
].filter(Boolean);
|
||||||
|
if (preProcessor) {
|
||||||
|
loaders.push({
|
||||||
|
loader: require.resolve(preProcessor),
|
||||||
|
options: {
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return loaders;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode: isEnvProduction ? "production" : isEnvDevelopment && "development",
|
||||||
|
// Stop compilation early in production
|
||||||
|
bail: isEnvProduction,
|
||||||
|
devtool: isEnvProduction ? (shouldUseSourceMap ? "source-map" : false) : isEnvDevelopment && "eval-source-map",
|
||||||
|
// These are the "entry points" to our application.
|
||||||
|
// This means they will be the "root" imports that are included in JS bundle.
|
||||||
|
entry: [
|
||||||
|
// Include an alternative client for WebpackDevServer. A client's job is to
|
||||||
|
// connect to WebpackDevServer by a socket and get notified about changes.
|
||||||
|
// When you save a file, the client will either apply hot updates (in case
|
||||||
|
// of CSS changes), or refresh the page (in case of JS changes). When you
|
||||||
|
// make a syntax error, this client will display a syntax error overlay.
|
||||||
|
// Note: instead of the default WebpackDevServer client, we use a custom one
|
||||||
|
// to bring better experience for Create React App users. You can replace
|
||||||
|
// the line below with these two lines if you prefer the stock client:
|
||||||
|
// require.resolve('webpack-dev-server/client') + '?/',
|
||||||
|
// require.resolve('webpack/hot/dev-server'),
|
||||||
|
isEnvDevelopment && require.resolve("react-dev-utils/webpackHotDevClient"),
|
||||||
|
// Finally, this is your app's code:
|
||||||
|
paths.appIndexJs,
|
||||||
|
// We include the app code last so that if there is a runtime error during
|
||||||
|
// initialization, it doesn't blow up the WebpackDevServer client, and
|
||||||
|
// changing JS code would still trigger a refresh.
|
||||||
|
].filter(Boolean),
|
||||||
|
output: {
|
||||||
|
// The build folder.
|
||||||
|
path: isEnvProduction ? paths.appBuild : undefined,
|
||||||
|
// Add /* filename */ comments to generated require()s in the output.
|
||||||
|
pathinfo: isEnvDevelopment,
|
||||||
|
// There will be one main bundle, and one file per asynchronous chunk.
|
||||||
|
// In development, it does not produce real files.
|
||||||
|
filename: isEnvProduction ? "static/js/[name].[chunkhash:8].js" : isEnvDevelopment && "static/js/bundle.js",
|
||||||
|
// There are also additional JS chunk files if you use code splitting.
|
||||||
|
chunkFilename: isEnvProduction
|
||||||
|
? "static/js/[name].[chunkhash:8].chunk.js"
|
||||||
|
: isEnvDevelopment && "static/js/[name].chunk.js",
|
||||||
|
// We inferred the "public path" (such as / or /my-project) from homepage.
|
||||||
|
// We use "/" in development.
|
||||||
|
publicPath: publicPath,
|
||||||
|
// Point sourcemap entries to original disk location (format as URL on Windows)
|
||||||
|
devtoolModuleFilenameTemplate: isEnvProduction
|
||||||
|
? (info) => path.relative(paths.appSrc, info.absoluteResourcePath).replace(/\\/g, "/")
|
||||||
|
: isEnvDevelopment && ((info) => path.resolve(info.absoluteResourcePath).replace(/\\/g, "/")),
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: isEnvProduction,
|
||||||
|
minimizer: [
|
||||||
|
// This is only used in production mode
|
||||||
|
new TerserPlugin({
|
||||||
|
terserOptions: {
|
||||||
|
parse: {
|
||||||
|
// we want terser to parse ecma 8 code. However, we don't want it
|
||||||
|
// to apply any minfication steps that turns valid ecma 5 code
|
||||||
|
// into invalid ecma 5 code. This is why the 'compress' and 'output'
|
||||||
|
// sections only apply transformations that are ecma 5 safe
|
||||||
|
// https://github.com/facebook/create-react-app/pull/4234
|
||||||
|
ecma: 8,
|
||||||
|
},
|
||||||
|
compress: {
|
||||||
|
ecma: 5,
|
||||||
|
warnings: false,
|
||||||
|
// Disabled because of an issue with Uglify breaking seemingly valid code:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2376
|
||||||
|
// Pending further investigation:
|
||||||
|
// https://github.com/mishoo/UglifyJS2/issues/2011
|
||||||
|
comparisons: false,
|
||||||
|
// Disabled because of an issue with Terser breaking valid code:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/5250
|
||||||
|
// Pending futher investigation:
|
||||||
|
// https://github.com/terser-js/terser/issues/120
|
||||||
|
inline: 2,
|
||||||
|
},
|
||||||
|
mangle: {
|
||||||
|
safari10: true,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
ecma: 5,
|
||||||
|
comments: false,
|
||||||
|
// Turned on because emoji and regex is not minified properly using default
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2488
|
||||||
|
ascii_only: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Use multi-process parallel running to improve the build speed
|
||||||
|
// Default number of concurrent runs: os.cpus().length - 1
|
||||||
|
parallel: true,
|
||||||
|
// Enable file caching
|
||||||
|
cache: true,
|
||||||
|
sourceMap: shouldUseSourceMap,
|
||||||
|
}),
|
||||||
|
// This is only used in production mode
|
||||||
|
new OptimizeCSSAssetsPlugin({
|
||||||
|
cssProcessorOptions: {
|
||||||
|
parser: safePostCssParser,
|
||||||
|
map: shouldUseSourceMap
|
||||||
|
? {
|
||||||
|
// `inline: false` forces the sourcemap to be output into a
|
||||||
|
// separate file
|
||||||
|
inline: false,
|
||||||
|
// `annotation: true` appends the sourceMappingURL to the end of
|
||||||
|
// the css file, helping the browser find the sourcemap
|
||||||
|
annotation: true,
|
||||||
|
}
|
||||||
|
: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
// Automatically split vendor and commons
|
||||||
|
// https://twitter.com/wSokra/status/969633336732905474
|
||||||
|
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
|
||||||
|
splitChunks: {
|
||||||
|
chunks: "all",
|
||||||
|
name: false,
|
||||||
|
},
|
||||||
|
// Keep the runtime chunk separated to enable long term caching
|
||||||
|
// https://twitter.com/wSokra/status/969679223278505985
|
||||||
|
runtimeChunk: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
// This allows you to set a fallback for where Webpack should look for modules.
|
||||||
|
// We placed these paths second because we want `node_modules` to "win"
|
||||||
|
// if there are any conflicts. This matches Node resolution mechanism.
|
||||||
|
// https://github.com/facebook/create-react-app/issues/253
|
||||||
|
modules: ["node_modules"].concat(
|
||||||
|
// It is guaranteed to exist because we tweak it in `env.js`
|
||||||
|
process.env.NODE_PATH.split(path.delimiter).filter(Boolean),
|
||||||
|
),
|
||||||
|
// These are the reasonable defaults supported by the Node ecosystem.
|
||||||
|
// We also include JSX as a common component filename extension to support
|
||||||
|
// some tools, although we do not recommend using it, see:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/290
|
||||||
|
// `web` extension prefixes have been added for better support
|
||||||
|
// for React Native Web.
|
||||||
|
extensions: paths.moduleFileExtensions
|
||||||
|
.map((ext) => `.${ext}`)
|
||||||
|
.filter((ext) => useTypeScript || !ext.includes("ts")),
|
||||||
|
alias: {
|
||||||
|
// Support React Native Web
|
||||||
|
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
|
||||||
|
"react-native": "react-native-web",
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// Adds support for installing with Plug'n'Play, leading to faster installs and adding
|
||||||
|
// guards against forgotten dependencies and such.
|
||||||
|
PnpWebpackPlugin,
|
||||||
|
// Prevents users from importing files from outside of src/ (or node_modules/).
|
||||||
|
// This often causes confusion because we only process files within src/ with babel.
|
||||||
|
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
|
||||||
|
// please link the files into your node_modules/ and let module-resolution kick in.
|
||||||
|
// Make sure your source files are compiled, as they will not be processed in any way.
|
||||||
|
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolveLoader: {
|
||||||
|
plugins: [
|
||||||
|
// Also related to Plug'n'Play, but this time it tells Webpack to load its loaders
|
||||||
|
// from the current package.
|
||||||
|
PnpWebpackPlugin.moduleLoader(module),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
strictExportPresence: true,
|
||||||
|
rules: [
|
||||||
|
// Disable require.ensure as it's not a standard language feature.
|
||||||
|
{parser: {requireEnsure: false}},
|
||||||
|
{
|
||||||
|
// "oneOf" will traverse all following loaders until one will
|
||||||
|
// match the requirements. When no loader matches it will fall
|
||||||
|
// back to the "file" loader at the end of the loader list.
|
||||||
|
oneOf: [
|
||||||
|
// "url" loader works like "file" loader except that it embeds assets
|
||||||
|
// smaller than specified limit in bytes as data URLs to avoid requests.
|
||||||
|
// A missing `test` is equivalent to a match.
|
||||||
|
{
|
||||||
|
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
|
||||||
|
loader: require.resolve("url-loader"),
|
||||||
|
options: {
|
||||||
|
limit: 10000,
|
||||||
|
name: "static/media/[name].[hash:8].[ext]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Process application JS with Babel.
|
||||||
|
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
|
||||||
|
{
|
||||||
|
test: /\.(js|mjs|jsx|ts|tsx)$/,
|
||||||
|
include: paths.appSrc,
|
||||||
|
loader: require.resolve("babel-loader"),
|
||||||
|
options: {
|
||||||
|
customize: require.resolve("babel-preset-react-app/webpack-overrides"),
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
require.resolve("babel-plugin-named-asset-import"),
|
||||||
|
{
|
||||||
|
loaderMap: {
|
||||||
|
svg: {
|
||||||
|
ReactComponent: "@svgr/webpack?-svgo![path]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
// This is a feature of `babel-loader` for webpack (not Babel itself).
|
||||||
|
// It enables caching results in ./node_modules/.cache/babel-loader/
|
||||||
|
// directory for faster rebuilds.
|
||||||
|
cacheDirectory: true,
|
||||||
|
cacheCompression: isEnvProduction,
|
||||||
|
compact: isEnvProduction,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Process any JS outside of the app with Babel.
|
||||||
|
// Unlike the application JS, we only compile the standard ES features.
|
||||||
|
{
|
||||||
|
test: /\.(js|mjs)$/,
|
||||||
|
exclude: /@babel(?:\/|\\{1,2})runtime/,
|
||||||
|
loader: require.resolve("babel-loader"),
|
||||||
|
options: {
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
compact: false,
|
||||||
|
presets: [[require.resolve("babel-preset-react-app/dependencies"), {helpers: true}]],
|
||||||
|
cacheDirectory: true,
|
||||||
|
cacheCompression: isEnvProduction,
|
||||||
|
|
||||||
|
// If an error happens in a package, it's possible to be
|
||||||
|
// because it was compiled. Thus, we don't want the browser
|
||||||
|
// debugger to show the original code. Instead, the code
|
||||||
|
// being evaluated would be much more helpful.
|
||||||
|
sourceMaps: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// "postcss" loader applies autoprefixer to our CSS.
|
||||||
|
// "css" loader resolves paths in CSS and adds assets as dependencies.
|
||||||
|
// "style" loader turns CSS into JS modules that inject <style> tags.
|
||||||
|
// In production, we use MiniCSSExtractPlugin to extract that CSS
|
||||||
|
// to a file, but in development "style" loader enables hot editing
|
||||||
|
// of CSS.
|
||||||
|
// By default we support CSS Modules with the extension .module.css
|
||||||
|
{
|
||||||
|
test: cssRegex,
|
||||||
|
exclude: cssModuleRegex,
|
||||||
|
use: getStyleLoaders({
|
||||||
|
importLoaders: 1,
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
}),
|
||||||
|
// Don't consider CSS imports dead code even if the
|
||||||
|
// containing package claims to have no side effects.
|
||||||
|
// Remove this when webpack adds a warning or an error for this.
|
||||||
|
// See https://github.com/webpack/webpack/issues/6571
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
|
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
|
||||||
|
// using the extension .module.css
|
||||||
|
{
|
||||||
|
test: cssModuleRegex,
|
||||||
|
use: getStyleLoaders({
|
||||||
|
importLoaders: 1,
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
modules: true,
|
||||||
|
getLocalIdent: getCSSModuleLocalIdent,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
// Opt-in support for SASS (using .scss or .sass extensions).
|
||||||
|
// By default we support SASS Modules with the
|
||||||
|
// extensions .module.scss or .module.sass
|
||||||
|
{
|
||||||
|
test: sassRegex,
|
||||||
|
exclude: sassModuleRegex,
|
||||||
|
use: getStyleLoaders(
|
||||||
|
{
|
||||||
|
importLoaders: 2,
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
},
|
||||||
|
"sass-loader",
|
||||||
|
),
|
||||||
|
// Don't consider CSS imports dead code even if the
|
||||||
|
// containing package claims to have no side effects.
|
||||||
|
// Remove this when webpack adds a warning or an error for this.
|
||||||
|
// See https://github.com/webpack/webpack/issues/6571
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
|
// Adds support for CSS Modules, but using SASS
|
||||||
|
// using the extension .module.scss or .module.sass
|
||||||
|
{
|
||||||
|
test: sassModuleRegex,
|
||||||
|
use: getStyleLoaders(
|
||||||
|
{
|
||||||
|
importLoaders: 2,
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
modules: true,
|
||||||
|
getLocalIdent: getCSSModuleLocalIdent,
|
||||||
|
},
|
||||||
|
"sass-loader",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// "file" loader makes sure those assets get served by WebpackDevServer.
|
||||||
|
// When you `import` an asset, you get its (virtual) filename.
|
||||||
|
// In production, they would get copied to the `build` folder.
|
||||||
|
// This loader doesn't use a "test" so it will catch all modules
|
||||||
|
// that fall through the other loaders.
|
||||||
|
{
|
||||||
|
loader: require.resolve("file-loader"),
|
||||||
|
// Exclude `js` files to keep "css" loader working as it injects
|
||||||
|
// its runtime that would otherwise be processed through "file" loader.
|
||||||
|
// Also exclude `html` and `json` extensions so they get processed
|
||||||
|
// by webpacks internal loaders.
|
||||||
|
exclude: [/\.(js|mjs|jsx|ts|tsx|md)$/, /\.html$/, /\.json$/],
|
||||||
|
options: {
|
||||||
|
name: "static/media/[name].[hash:8].[ext]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ** STOP ** Are you adding a new loader?
|
||||||
|
// Make sure to add the new loader(s) before the "file" loader.
|
||||||
|
|
||||||
|
{
|
||||||
|
loader: require.resolve("raw-loader"),
|
||||||
|
test: /\.md/i,
|
||||||
|
include: path.resolve('./src/template')
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// Generates an `index.html` file with the <script> injected.
|
||||||
|
new HtmlWebpackPlugin(
|
||||||
|
Object.assign(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
inject: true,
|
||||||
|
template: paths.appHtml,
|
||||||
|
},
|
||||||
|
isEnvProduction
|
||||||
|
? {
|
||||||
|
minify: {
|
||||||
|
removeComments: true,
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeRedundantAttributes: true,
|
||||||
|
useShortDoctype: true,
|
||||||
|
removeEmptyAttributes: true,
|
||||||
|
removeStyleLinkTypeAttributes: true,
|
||||||
|
keepClosingSlash: true,
|
||||||
|
minifyJS: true,
|
||||||
|
minifyCSS: true,
|
||||||
|
minifyURLs: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Inlines the webpack runtime script. This script is too small to warrant
|
||||||
|
// a network request.
|
||||||
|
isEnvProduction && shouldInlineRuntimeChunk && new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime~.+[.]js/]),
|
||||||
|
// Makes some environment variables available in index.html.
|
||||||
|
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||||
|
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||||
|
// In production, it will be an empty string unless you specify "homepage"
|
||||||
|
// in `package.json`, in which case it will be the pathname of that URL.
|
||||||
|
// In development, this will be an empty string.
|
||||||
|
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
|
||||||
|
// This gives some necessary context to module not found errors, such as
|
||||||
|
// the requesting resource.
|
||||||
|
new ModuleNotFoundPlugin(paths.appPath),
|
||||||
|
// Makes some environment variables available to the JS code, for example:
|
||||||
|
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
|
||||||
|
// It is absolutely essential that NODE_ENV is set to production
|
||||||
|
// during a production build.
|
||||||
|
// Otherwise React will be compiled in the very slow development mode.
|
||||||
|
new webpack.DefinePlugin(env.stringified),
|
||||||
|
// This is necessary to emit hot updates (currently CSS only):
|
||||||
|
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
|
||||||
|
// Watcher doesn't work well if you mistype casing in a path so we use
|
||||||
|
// a plugin that prints an error when you attempt to do this.
|
||||||
|
// See https://github.com/facebook/create-react-app/issues/240
|
||||||
|
isEnvDevelopment && new CaseSensitivePathsPlugin(),
|
||||||
|
// If you require a missing module and then `npm install` it, you still have
|
||||||
|
// to restart the development server for Webpack to discover it. This plugin
|
||||||
|
// makes the discovery automatic so you don't have to restart.
|
||||||
|
// See https://github.com/facebook/create-react-app/issues/186
|
||||||
|
isEnvDevelopment && new WatchMissingNodeModulesPlugin(paths.appNodeModules),
|
||||||
|
isEnvProduction &&
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
// Options similar to the same options in webpackOptions.output
|
||||||
|
// both options are optional
|
||||||
|
filename: "static/css/[name].[contenthash:8].css",
|
||||||
|
chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
|
||||||
|
}),
|
||||||
|
// Generate a manifest file which contains a mapping of all asset filenames
|
||||||
|
// to their corresponding output file so that tools can pick it up without
|
||||||
|
// having to parse `index.html`.
|
||||||
|
new ManifestPlugin({
|
||||||
|
fileName: "asset-manifest.json",
|
||||||
|
publicPath: publicPath,
|
||||||
|
}),
|
||||||
|
// Moment.js is an extremely popular library that bundles large locale files
|
||||||
|
// by default due to how Webpack interprets its code. This is a practical
|
||||||
|
// solution that requires the user to opt into importing specific locales.
|
||||||
|
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
|
||||||
|
// You can remove this if you don't use Moment.js:
|
||||||
|
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||||
|
// Generate a service worker script that will precache, and keep up to date,
|
||||||
|
// the HTML & assets that are part of the Webpack build.
|
||||||
|
isEnvProduction &&
|
||||||
|
new WorkboxWebpackPlugin.GenerateSW({
|
||||||
|
clientsClaim: true,
|
||||||
|
skipWaiting: true,
|
||||||
|
exclude: [/\.map$/, /asset-manifest\.json$/],
|
||||||
|
importWorkboxFrom: 'local',
|
||||||
|
navigateFallback: publicUrl + '/index.html',
|
||||||
|
navigateFallbackBlacklist: [
|
||||||
|
// Exclude URLs starting with /_, as they're likely an API call
|
||||||
|
new RegExp("^/_"),
|
||||||
|
// Exclude URLs containing a dot, as they're likely a resource in
|
||||||
|
// public/ and not a SPA route
|
||||||
|
new RegExp("/[^/]+\\.[^/]+$"),
|
||||||
|
],
|
||||||
|
runtimeCaching: [
|
||||||
|
// 配置路由请求缓存 对应 workbox.routing.registerRoute
|
||||||
|
{
|
||||||
|
urlPattern: /.*\.js/, // 匹配文件
|
||||||
|
handler: "networkFirst", // 网络优先
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPattern: /.*\.css/,
|
||||||
|
handler: "staleWhileRevalidate", // 缓存优先同时后台更新
|
||||||
|
options: {
|
||||||
|
// 这里可以设置 cacheName 和添加插件
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
cacheableResponse: {
|
||||||
|
statuses: [0, 200],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPattern: /.*\.(png|jpg|jpeg|svg|gif)/,
|
||||||
|
handler: "cacheFirst", // 缓存优先
|
||||||
|
options: {
|
||||||
|
cacheName: 'images',
|
||||||
|
expiration: {
|
||||||
|
maxAgeSeconds: 24 * 60 * 60, // 最长缓存时间,
|
||||||
|
maxEntries: 50, // 最大缓存图片数量
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPattern: /.*\.html/,
|
||||||
|
handler: "networkFirst",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
// TypeScript type checking
|
||||||
|
useTypeScript &&
|
||||||
|
new ForkTsCheckerWebpackPlugin({
|
||||||
|
typescript: resolve.sync("typescript", {
|
||||||
|
basedir: paths.appNodeModules,
|
||||||
|
}),
|
||||||
|
async: false,
|
||||||
|
checkSyntacticErrors: true,
|
||||||
|
tsconfig: paths.appTsConfig,
|
||||||
|
compilerOptions: {
|
||||||
|
module: "esnext",
|
||||||
|
moduleResolution: "node",
|
||||||
|
resolveJsonModule: true,
|
||||||
|
isolatedModules: true,
|
||||||
|
noEmit: true,
|
||||||
|
jsx: "preserve",
|
||||||
|
},
|
||||||
|
reportFiles: [
|
||||||
|
"**",
|
||||||
|
"!**/*.json",
|
||||||
|
"!**/__tests__/**",
|
||||||
|
"!**/?(*.)(spec|test).*",
|
||||||
|
"!**/src/setupProxy.*",
|
||||||
|
"!**/src/setupTests.*",
|
||||||
|
],
|
||||||
|
watch: paths.appSrc,
|
||||||
|
silent: true,
|
||||||
|
formatter: typescriptFormatter,
|
||||||
|
}),
|
||||||
|
].filter(Boolean),
|
||||||
|
// Some libraries import Node modules but don't use them in the browser.
|
||||||
|
// Tell Webpack to provide empty mocks for them so importing them works.
|
||||||
|
node: {
|
||||||
|
module: "empty",
|
||||||
|
dgram: "empty",
|
||||||
|
dns: "mock",
|
||||||
|
fs: "empty",
|
||||||
|
net: "empty",
|
||||||
|
tls: "empty",
|
||||||
|
child_process: "empty",
|
||||||
|
},
|
||||||
|
// Turn off performance processing because we utilize
|
||||||
|
// our own hints via the FileSizeReporter
|
||||||
|
performance: false,
|
||||||
|
};
|
||||||
|
};
|
566
config/webpack.config.lib.js
Normal file
566
config/webpack.config.lib.js
Normal file
@ -0,0 +1,566 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const webpack = require("webpack");
|
||||||
|
const resolve = require("resolve");
|
||||||
|
const PnpWebpackPlugin = require("pnp-webpack-plugin");
|
||||||
|
const CaseSensitivePathsPlugin = require("case-sensitive-paths-webpack-plugin");
|
||||||
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
|
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||||
|
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
|
||||||
|
const safePostCssParser = require("postcss-safe-parser");
|
||||||
|
const ManifestPlugin = require("webpack-manifest-plugin");
|
||||||
|
const WorkboxWebpackPlugin = require("workbox-webpack-plugin");
|
||||||
|
const WatchMissingNodeModulesPlugin = require("react-dev-utils/WatchMissingNodeModulesPlugin");
|
||||||
|
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin");
|
||||||
|
const getCSSModuleLocalIdent = require("react-dev-utils/getCSSModuleLocalIdent");
|
||||||
|
const paths = require("./paths");
|
||||||
|
const getClientEnvironment = require("./env");
|
||||||
|
const ModuleNotFoundPlugin = require("react-dev-utils/ModuleNotFoundPlugin");
|
||||||
|
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin-alt");
|
||||||
|
const typescriptFormatter = require("react-dev-utils/typescriptFormatter");
|
||||||
|
|
||||||
|
// Source maps are resource heavy and can cause out of memory issue for large source files.
|
||||||
|
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false";
|
||||||
|
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
|
||||||
|
// makes for a smoother build process.
|
||||||
|
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== "false";
|
||||||
|
|
||||||
|
// Check if TypeScript is setup
|
||||||
|
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||||
|
|
||||||
|
// style files regexes
|
||||||
|
const cssRegex = /\.css$/;
|
||||||
|
const cssModuleRegex = /\.module\.css$/;
|
||||||
|
const sassRegex = /\.(scss|sass)$/;
|
||||||
|
const sassModuleRegex = /\.module\.(scss|sass)$/;
|
||||||
|
|
||||||
|
// This is the production and development configuration.
|
||||||
|
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
|
||||||
|
module.exports = function(webpackEnv) {
|
||||||
|
const isEnvDevelopment = webpackEnv === "development";
|
||||||
|
const isEnvProduction = webpackEnv === "production";
|
||||||
|
|
||||||
|
// Webpack uses `publicPath` to determine where the app is being served from.
|
||||||
|
// It requires a trailing slash, or the file assets will get an incorrect path.
|
||||||
|
// In development, we always serve from the root. This makes config easier.
|
||||||
|
const publicPath = isEnvProduction ? paths.servedPath : isEnvDevelopment && "/";
|
||||||
|
// Some apps do not use client-side routing with pushState.
|
||||||
|
// For these, "homepage" can be set to "." to enable relative asset paths.
|
||||||
|
const shouldUseRelativeAssetPaths = publicPath === "./";
|
||||||
|
|
||||||
|
// `publicUrl` is just like `publicPath`, but we will provide it to our app
|
||||||
|
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
|
||||||
|
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
|
||||||
|
const publicUrl = isEnvProduction ? publicPath.slice(0, -1) : isEnvDevelopment && "";
|
||||||
|
// Get environment variables to inject into our app.
|
||||||
|
const env = getClientEnvironment(publicUrl);
|
||||||
|
|
||||||
|
// common function to get style loaders
|
||||||
|
const getStyleLoaders = (cssOptions, preProcessor) => {
|
||||||
|
const loaders = [
|
||||||
|
isEnvDevelopment && require.resolve("style-loader"),
|
||||||
|
isEnvProduction && {
|
||||||
|
loader: MiniCssExtractPlugin.loader,
|
||||||
|
options: Object.assign({}, shouldUseRelativeAssetPaths ? {publicPath: "../../"} : undefined),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: require.resolve("css-loader"),
|
||||||
|
options: cssOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Options for PostCSS as we reference these options twice
|
||||||
|
// Adds vendor prefixing based on your specified browser support in
|
||||||
|
// package.json
|
||||||
|
loader: require.resolve("postcss-loader"),
|
||||||
|
options: {
|
||||||
|
// Necessary for external CSS imports to work
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2677
|
||||||
|
ident: "postcss",
|
||||||
|
plugins: () => [
|
||||||
|
require("postcss-flexbugs-fixes"),
|
||||||
|
require("postcss-preset-env")({
|
||||||
|
autoprefixer: {
|
||||||
|
flexbox: "no-2009",
|
||||||
|
},
|
||||||
|
stage: 3,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
].filter(Boolean);
|
||||||
|
if (preProcessor) {
|
||||||
|
loaders.push({
|
||||||
|
loader: require.resolve(preProcessor),
|
||||||
|
options: {
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return loaders;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode: isEnvProduction ? "production" : isEnvDevelopment && "development",
|
||||||
|
// Stop compilation early in production
|
||||||
|
bail: isEnvProduction,
|
||||||
|
devtool: isEnvProduction ? (shouldUseSourceMap ? "source-map" : false) : isEnvDevelopment && "eval-source-map",
|
||||||
|
// These are the "entry points" to our application.
|
||||||
|
// This means they will be the "root" imports that are included in JS bundle.
|
||||||
|
entry: [
|
||||||
|
// Include an alternative client for WebpackDevServer. A client's job is to
|
||||||
|
// connect to WebpackDevServer by a socket and get notified about changes.
|
||||||
|
// When you save a file, the client will either apply hot updates (in case
|
||||||
|
// of CSS changes), or refresh the page (in case of JS changes). When you
|
||||||
|
// make a syntax error, this client will display a syntax error overlay.
|
||||||
|
// Note: instead of the default WebpackDevServer client, we use a custom one
|
||||||
|
// to bring better experience for Create React App users. You can replace
|
||||||
|
// the line below with these two lines if you prefer the stock client:
|
||||||
|
// require.resolve('webpack-dev-server/client') + '?/',
|
||||||
|
// require.resolve('webpack/hot/dev-server'),
|
||||||
|
isEnvDevelopment && require.resolve("react-dev-utils/webpackHotDevClient"),
|
||||||
|
// Finally, this is your app's code:
|
||||||
|
paths.appLib,
|
||||||
|
// We include the app code last so that if there is a runtime error during
|
||||||
|
// initialization, it doesn't blow up the WebpackDevServer client, and
|
||||||
|
// changing JS code would still trigger a refresh.
|
||||||
|
].filter(Boolean),
|
||||||
|
output: {
|
||||||
|
// The build folder.
|
||||||
|
path: isEnvProduction ? paths.appBuild : undefined,
|
||||||
|
// Add /* filename */ comments to generated require()s in the output.
|
||||||
|
pathinfo: isEnvDevelopment,
|
||||||
|
// There will be one main bundle, and one file per asynchronous chunk.
|
||||||
|
// In development, it does not produce real files.
|
||||||
|
filename: isEnvProduction ? "static/js/[name].[chunkhash:8].js" : isEnvDevelopment && "static/js/bundle.js",
|
||||||
|
// There are also additional JS chunk files if you use code splitting.
|
||||||
|
chunkFilename: isEnvProduction
|
||||||
|
? "static/js/[name].[chunkhash:8].chunk.js"
|
||||||
|
: isEnvDevelopment && "static/js/[name].chunk.js",
|
||||||
|
// We inferred the "public path" (such as / or /my-project) from homepage.
|
||||||
|
// We use "/" in development.
|
||||||
|
publicPath: publicPath,
|
||||||
|
// Point sourcemap entries to original disk location (format as URL on Windows)
|
||||||
|
devtoolModuleFilenameTemplate: isEnvProduction
|
||||||
|
? (info) => path.relative(paths.appSrc, info.absoluteResourcePath).replace(/\\/g, "/")
|
||||||
|
: isEnvDevelopment && ((info) => path.resolve(info.absoluteResourcePath).replace(/\\/g, "/")),
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: isEnvProduction,
|
||||||
|
minimizer: [
|
||||||
|
// This is only used in production mode
|
||||||
|
new TerserPlugin({
|
||||||
|
terserOptions: {
|
||||||
|
parse: {
|
||||||
|
// we want terser to parse ecma 8 code. However, we don't want it
|
||||||
|
// to apply any minfication steps that turns valid ecma 5 code
|
||||||
|
// into invalid ecma 5 code. This is why the 'compress' and 'output'
|
||||||
|
// sections only apply transformations that are ecma 5 safe
|
||||||
|
// https://github.com/facebook/create-react-app/pull/4234
|
||||||
|
ecma: 8,
|
||||||
|
},
|
||||||
|
compress: {
|
||||||
|
ecma: 5,
|
||||||
|
warnings: false,
|
||||||
|
// Disabled because of an issue with Uglify breaking seemingly valid code:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2376
|
||||||
|
// Pending further investigation:
|
||||||
|
// https://github.com/mishoo/UglifyJS2/issues/2011
|
||||||
|
comparisons: false,
|
||||||
|
// Disabled because of an issue with Terser breaking valid code:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/5250
|
||||||
|
// Pending futher investigation:
|
||||||
|
// https://github.com/terser-js/terser/issues/120
|
||||||
|
inline: 2,
|
||||||
|
},
|
||||||
|
mangle: {
|
||||||
|
safari10: true,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
ecma: 5,
|
||||||
|
comments: false,
|
||||||
|
// Turned on because emoji and regex is not minified properly using default
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2488
|
||||||
|
ascii_only: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Use multi-process parallel running to improve the build speed
|
||||||
|
// Default number of concurrent runs: os.cpus().length - 1
|
||||||
|
parallel: true,
|
||||||
|
// Enable file caching
|
||||||
|
cache: true,
|
||||||
|
sourceMap: shouldUseSourceMap,
|
||||||
|
}),
|
||||||
|
// This is only used in production mode
|
||||||
|
new OptimizeCSSAssetsPlugin({
|
||||||
|
cssProcessorOptions: {
|
||||||
|
parser: safePostCssParser,
|
||||||
|
map: shouldUseSourceMap
|
||||||
|
? {
|
||||||
|
// `inline: false` forces the sourcemap to be output into a
|
||||||
|
// separate file
|
||||||
|
inline: false,
|
||||||
|
// `annotation: true` appends the sourceMappingURL to the end of
|
||||||
|
// the css file, helping the browser find the sourcemap
|
||||||
|
annotation: true,
|
||||||
|
}
|
||||||
|
: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
// Automatically split vendor and commons
|
||||||
|
// https://twitter.com/wSokra/status/969633336732905474
|
||||||
|
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
|
||||||
|
splitChunks: {
|
||||||
|
chunks: "all",
|
||||||
|
name: false,
|
||||||
|
},
|
||||||
|
// Keep the runtime chunk separated to enable long term caching
|
||||||
|
// https://twitter.com/wSokra/status/969679223278505985
|
||||||
|
runtimeChunk: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
// This allows you to set a fallback for where Webpack should look for modules.
|
||||||
|
// We placed these paths second because we want `node_modules` to "win"
|
||||||
|
// if there are any conflicts. This matches Node resolution mechanism.
|
||||||
|
// https://github.com/facebook/create-react-app/issues/253
|
||||||
|
modules: ["node_modules"].concat(
|
||||||
|
// It is guaranteed to exist because we tweak it in `env.js`
|
||||||
|
process.env.NODE_PATH.split(path.delimiter).filter(Boolean),
|
||||||
|
),
|
||||||
|
// These are the reasonable defaults supported by the Node ecosystem.
|
||||||
|
// We also include JSX as a common component filename extension to support
|
||||||
|
// some tools, although we do not recommend using it, see:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/290
|
||||||
|
// `web` extension prefixes have been added for better support
|
||||||
|
// for React Native Web.
|
||||||
|
extensions: paths.moduleFileExtensions
|
||||||
|
.map((ext) => `.${ext}`)
|
||||||
|
.filter((ext) => useTypeScript || !ext.includes("ts")),
|
||||||
|
alias: {
|
||||||
|
// Support React Native Web
|
||||||
|
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
|
||||||
|
"react-native": "react-native-web",
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// Adds support for installing with Plug'n'Play, leading to faster installs and adding
|
||||||
|
// guards against forgotten dependencies and such.
|
||||||
|
PnpWebpackPlugin,
|
||||||
|
// Prevents users from importing files from outside of src/ (or node_modules/).
|
||||||
|
// This often causes confusion because we only process files within src/ with babel.
|
||||||
|
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
|
||||||
|
// please link the files into your node_modules/ and let module-resolution kick in.
|
||||||
|
// Make sure your source files are compiled, as they will not be processed in any way.
|
||||||
|
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolveLoader: {
|
||||||
|
plugins: [
|
||||||
|
// Also related to Plug'n'Play, but this time it tells Webpack to load its loaders
|
||||||
|
// from the current package.
|
||||||
|
PnpWebpackPlugin.moduleLoader(module),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
strictExportPresence: true,
|
||||||
|
rules: [
|
||||||
|
// Disable require.ensure as it's not a standard language feature.
|
||||||
|
{parser: {requireEnsure: false}},
|
||||||
|
|
||||||
|
// First, run the linter.
|
||||||
|
// It's important to do this before Babel processes the JS.
|
||||||
|
{
|
||||||
|
// "oneOf" will traverse all following loaders until one will
|
||||||
|
// match the requirements. When no loader matches it will fall
|
||||||
|
// back to the "file" loader at the end of the loader list.
|
||||||
|
oneOf: [
|
||||||
|
// "url" loader works like "file" loader except that it embeds assets
|
||||||
|
// smaller than specified limit in bytes as data URLs to avoid requests.
|
||||||
|
// A missing `test` is equivalent to a match.
|
||||||
|
{
|
||||||
|
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
|
||||||
|
loader: require.resolve("url-loader"),
|
||||||
|
options: {
|
||||||
|
limit: 10000,
|
||||||
|
name: "static/media/[name].[hash:8].[ext]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Process application JS with Babel.
|
||||||
|
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
|
||||||
|
{
|
||||||
|
test: /\.(js|mjs|jsx|ts|tsx)$/,
|
||||||
|
include: paths.appSrc,
|
||||||
|
use: [
|
||||||
|
"thread-loader",
|
||||||
|
{
|
||||||
|
loader: require.resolve("babel-loader"),
|
||||||
|
options: {
|
||||||
|
customize: require.resolve("babel-preset-react-app/webpack-overrides"),
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
require.resolve("babel-plugin-named-asset-import"),
|
||||||
|
{
|
||||||
|
loaderMap: {
|
||||||
|
svg: {
|
||||||
|
ReactComponent: "@svgr/webpack?-svgo![path]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
cacheDirectory: true,
|
||||||
|
cacheCompression: isEnvProduction,
|
||||||
|
compact: isEnvProduction,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Process any JS outside of the app with Babel.
|
||||||
|
// Unlike the application JS, we only compile the standard ES features.
|
||||||
|
{
|
||||||
|
test: /\.(js|mjs)$/,
|
||||||
|
exclude: /@babel(?:\/|\\{1,2})runtime/,
|
||||||
|
use: [
|
||||||
|
"thread-loader",
|
||||||
|
{
|
||||||
|
loader: require.resolve("babel-loader"),
|
||||||
|
options: {
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
compact: false,
|
||||||
|
presets: [[require.resolve("babel-preset-react-app/dependencies"), {helpers: true}]],
|
||||||
|
cacheDirectory: true,
|
||||||
|
cacheCompression: isEnvProduction,
|
||||||
|
sourceMaps: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// "postcss" loader applies autoprefixer to our CSS.
|
||||||
|
// "css" loader resolves paths in CSS and adds assets as dependencies.
|
||||||
|
// "style" loader turns CSS into JS modules that inject <style> tags.
|
||||||
|
// In production, we use MiniCSSExtractPlugin to extract that CSS
|
||||||
|
// to a file, but in development "style" loader enables hot editing
|
||||||
|
// of CSS.
|
||||||
|
// By default we support CSS Modules with the extension .module.css
|
||||||
|
{
|
||||||
|
test: cssRegex,
|
||||||
|
exclude: cssModuleRegex,
|
||||||
|
use: getStyleLoaders({
|
||||||
|
importLoaders: 1,
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
}),
|
||||||
|
// Don't consider CSS imports dead code even if the
|
||||||
|
// containing package claims to have no side effects.
|
||||||
|
// Remove this when webpack adds a warning or an error for this.
|
||||||
|
// See https://github.com/webpack/webpack/issues/6571
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
|
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
|
||||||
|
// using the extension .module.css
|
||||||
|
{
|
||||||
|
test: cssModuleRegex,
|
||||||
|
use: getStyleLoaders({
|
||||||
|
importLoaders: 1,
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
modules: true,
|
||||||
|
getLocalIdent: getCSSModuleLocalIdent,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
// Opt-in support for SASS (using .scss or .sass extensions).
|
||||||
|
// By default we support SASS Modules with the
|
||||||
|
// extensions .module.scss or .module.sass
|
||||||
|
{
|
||||||
|
test: sassRegex,
|
||||||
|
exclude: sassModuleRegex,
|
||||||
|
use: getStyleLoaders(
|
||||||
|
{
|
||||||
|
importLoaders: 2,
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
},
|
||||||
|
"sass-loader",
|
||||||
|
),
|
||||||
|
// Don't consider CSS imports dead code even if the
|
||||||
|
// containing package claims to have no side effects.
|
||||||
|
// Remove this when webpack adds a warning or an error for this.
|
||||||
|
// See https://github.com/webpack/webpack/issues/6571
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
|
// Adds support for CSS Modules, but using SASS
|
||||||
|
// using the extension .module.scss or .module.sass
|
||||||
|
{
|
||||||
|
test: sassModuleRegex,
|
||||||
|
use: getStyleLoaders(
|
||||||
|
{
|
||||||
|
importLoaders: 2,
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
modules: true,
|
||||||
|
getLocalIdent: getCSSModuleLocalIdent,
|
||||||
|
},
|
||||||
|
"sass-loader",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// "file" loader makes sure those assets get served by WebpackDevServer.
|
||||||
|
// When you `import` an asset, you get its (virtual) filename.
|
||||||
|
// In production, they would get copied to the `build` folder.
|
||||||
|
// This loader doesn't use a "test" so it will catch all modules
|
||||||
|
// that fall through the other loaders.
|
||||||
|
{
|
||||||
|
loader: require.resolve("file-loader"),
|
||||||
|
// Exclude `js` files to keep "css" loader working as it injects
|
||||||
|
// its runtime that would otherwise be processed through "file" loader.
|
||||||
|
// Also exclude `html` and `json` extensions so they get processed
|
||||||
|
// by webpacks internal loaders.
|
||||||
|
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
|
||||||
|
options: {
|
||||||
|
name: "static/media/[name].[hash:8].[ext]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ** STOP ** Are you adding a new loader?
|
||||||
|
// Make sure to add the new loader(s) before the "file" loader.
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// Generates an `index.html` file with the <script> injected.
|
||||||
|
// new HtmlWebpackPlugin(
|
||||||
|
// Object.assign(
|
||||||
|
// {},
|
||||||
|
// {
|
||||||
|
// inject: true,
|
||||||
|
// template: paths.appHtml,
|
||||||
|
// },
|
||||||
|
// isEnvProduction
|
||||||
|
// ? {
|
||||||
|
// minify: {
|
||||||
|
// removeComments: true,
|
||||||
|
// collapseWhitespace: true,
|
||||||
|
// removeRedundantAttributes: true,
|
||||||
|
// useShortDoctype: true,
|
||||||
|
// removeEmptyAttributes: true,
|
||||||
|
// removeStyleLinkTypeAttributes: true,
|
||||||
|
// keepClosingSlash: true,
|
||||||
|
// minifyJS: true,
|
||||||
|
// minifyCSS: true,
|
||||||
|
// minifyURLs: true,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// : undefined,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// Inlines the webpack runtime script. This script is too small to warrant
|
||||||
|
// a network request.
|
||||||
|
// isEnvProduction && shouldInlineRuntimeChunk && new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime~.+[.]js/]),
|
||||||
|
// Makes some environment variables available in index.html.
|
||||||
|
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||||
|
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||||
|
// In production, it will be an empty string unless you specify "homepage"
|
||||||
|
// in `package.json`, in which case it will be the pathname of that URL.
|
||||||
|
// In development, this will be an empty string.
|
||||||
|
// new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
|
||||||
|
// This gives some necessary context to module not found errors, such as
|
||||||
|
// the requesting resource.
|
||||||
|
new ModuleNotFoundPlugin(paths.appPath),
|
||||||
|
// Makes some environment variables available to the JS code, for example:
|
||||||
|
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
|
||||||
|
// It is absolutely essential that NODE_ENV is set to production
|
||||||
|
// during a production build.
|
||||||
|
// Otherwise React will be compiled in the very slow development mode.
|
||||||
|
new webpack.DefinePlugin(env.stringified),
|
||||||
|
// This is necessary to emit hot updates (currently CSS only):
|
||||||
|
// isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
|
||||||
|
// Watcher doesn't work well if you mistype casing in a path so we use
|
||||||
|
// a plugin that prints an error when you attempt to do this.
|
||||||
|
// See https://github.com/facebook/create-react-app/issues/240
|
||||||
|
isEnvDevelopment && new CaseSensitivePathsPlugin(),
|
||||||
|
// If you require a missing module and then `npm install` it, you still have
|
||||||
|
// to restart the development server for Webpack to discover it. This plugin
|
||||||
|
// makes the discovery automatic so you don't have to restart.
|
||||||
|
// See https://github.com/facebook/create-react-app/issues/186
|
||||||
|
isEnvDevelopment && new WatchMissingNodeModulesPlugin(paths.appNodeModules),
|
||||||
|
isEnvProduction &&
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
// Options similar to the same options in webpackOptions.output
|
||||||
|
// both options are optional
|
||||||
|
filename: "static/css/[name].[contenthash:8].css",
|
||||||
|
chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
|
||||||
|
}),
|
||||||
|
// Generate a manifest file which contains a mapping of all asset filenames
|
||||||
|
// to their corresponding output file so that tools can pick it up without
|
||||||
|
// having to parse `index.html`.
|
||||||
|
new ManifestPlugin({
|
||||||
|
fileName: "asset-manifest.json",
|
||||||
|
publicPath: publicPath,
|
||||||
|
}),
|
||||||
|
// Moment.js is an extremely popular library that bundles large locale files
|
||||||
|
// by default due to how Webpack interprets its code. This is a practical
|
||||||
|
// solution that requires the user to opt into importing specific locales.
|
||||||
|
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
|
||||||
|
// You can remove this if you don't use Moment.js:
|
||||||
|
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||||
|
// Generate a service worker script that will precache, and keep up to date,
|
||||||
|
// the HTML & assets that are part of the Webpack build.
|
||||||
|
isEnvProduction &&
|
||||||
|
new WorkboxWebpackPlugin.GenerateSW({
|
||||||
|
clientsClaim: true,
|
||||||
|
exclude: [/\.map$/, /asset-manifest\.json$/],
|
||||||
|
importWorkboxFrom: "cdn",
|
||||||
|
navigateFallback: publicUrl + "/index.html",
|
||||||
|
navigateFallbackBlacklist: [
|
||||||
|
// Exclude URLs starting with /_, as they're likely an API call
|
||||||
|
new RegExp("^/_"),
|
||||||
|
// Exclude URLs containing a dot, as they're likely a resource in
|
||||||
|
// public/ and not a SPA route
|
||||||
|
new RegExp("/[^/]+\\.[^/]+$"),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
// TypeScript type checking
|
||||||
|
useTypeScript &&
|
||||||
|
new ForkTsCheckerWebpackPlugin({
|
||||||
|
typescript: resolve.sync("typescript", {
|
||||||
|
basedir: paths.appNodeModules,
|
||||||
|
}),
|
||||||
|
async: false,
|
||||||
|
checkSyntacticErrors: true,
|
||||||
|
tsconfig: paths.appTsConfig,
|
||||||
|
compilerOptions: {
|
||||||
|
module: "esnext",
|
||||||
|
moduleResolution: "node",
|
||||||
|
resolveJsonModule: true,
|
||||||
|
isolatedModules: true,
|
||||||
|
noEmit: true,
|
||||||
|
jsx: "preserve",
|
||||||
|
},
|
||||||
|
reportFiles: [
|
||||||
|
"**",
|
||||||
|
"!**/*.json",
|
||||||
|
"!**/__tests__/**",
|
||||||
|
"!**/?(*.)(spec|test).*",
|
||||||
|
"!**/src/setupProxy.*",
|
||||||
|
"!**/src/setupTests.*",
|
||||||
|
],
|
||||||
|
watch: paths.appSrc,
|
||||||
|
silent: true,
|
||||||
|
formatter: typescriptFormatter,
|
||||||
|
}),
|
||||||
|
].filter(Boolean),
|
||||||
|
// Some libraries import Node modules but don't use them in the browser.
|
||||||
|
// Tell Webpack to provide empty mocks for them so importing them works.
|
||||||
|
node: {
|
||||||
|
module: "empty",
|
||||||
|
dgram: "empty",
|
||||||
|
dns: "mock",
|
||||||
|
fs: "empty",
|
||||||
|
net: "empty",
|
||||||
|
tls: "empty",
|
||||||
|
child_process: "empty",
|
||||||
|
},
|
||||||
|
// Turn off performance processing because we utilize
|
||||||
|
// our own hints via the FileSizeReporter
|
||||||
|
performance: false,
|
||||||
|
};
|
||||||
|
};
|
104
config/webpackDevServer.config.js
Normal file
104
config/webpackDevServer.config.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
|
||||||
|
const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
|
||||||
|
const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
|
||||||
|
const ignoredFiles = require('react-dev-utils/ignoredFiles');
|
||||||
|
const paths = require('./paths');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||||
|
const host = process.env.HOST || '0.0.0.0';
|
||||||
|
|
||||||
|
module.exports = function(proxy, allowedHost) {
|
||||||
|
return {
|
||||||
|
// WebpackDevServer 2.4.3 introduced a security fix that prevents remote
|
||||||
|
// websites from potentially accessing local content through DNS rebinding:
|
||||||
|
// https://github.com/webpack/webpack-dev-server/issues/887
|
||||||
|
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
|
||||||
|
// However, it made several existing use cases such as development in cloud
|
||||||
|
// environment or subdomains in development significantly more complicated:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2271
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2233
|
||||||
|
// While we're investigating better solutions, for now we will take a
|
||||||
|
// compromise. Since our WDS configuration only serves files in the `public`
|
||||||
|
// folder we won't consider accessing them a vulnerability. However, if you
|
||||||
|
// use the `proxy` feature, it gets more dangerous because it can expose
|
||||||
|
// remote code execution vulnerabilities in backends like Django and Rails.
|
||||||
|
// So we will disable the host check normally, but enable it if you have
|
||||||
|
// specified the `proxy` setting. Finally, we let you override it if you
|
||||||
|
// really know what you're doing with a special environment variable.
|
||||||
|
disableHostCheck:
|
||||||
|
!proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true',
|
||||||
|
// Enable gzip compression of generated files.
|
||||||
|
compress: true,
|
||||||
|
// Silence WebpackDevServer's own logs since they're generally not useful.
|
||||||
|
// It will still show compile warnings and errors with this setting.
|
||||||
|
clientLogLevel: 'none',
|
||||||
|
// By default WebpackDevServer serves physical files from current directory
|
||||||
|
// in addition to all the virtual build products that it serves from memory.
|
||||||
|
// This is confusing because those files won’t automatically be available in
|
||||||
|
// production build folder unless we copy them. However, copying the whole
|
||||||
|
// project directory is dangerous because we may expose sensitive files.
|
||||||
|
// Instead, we establish a convention that only files in `public` directory
|
||||||
|
// get served. Our build script will copy `public` into the `build` folder.
|
||||||
|
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
|
||||||
|
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||||
|
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
|
||||||
|
// Note that we only recommend to use `public` folder as an escape hatch
|
||||||
|
// for files like `favicon.ico`, `manifest.json`, and libraries that are
|
||||||
|
// for some reason broken when imported through Webpack. If you just want to
|
||||||
|
// use an image, put it in `src` and `import` it from JavaScript instead.
|
||||||
|
contentBase: paths.appPublic,
|
||||||
|
// By default files from `contentBase` will not trigger a page reload.
|
||||||
|
watchContentBase: true,
|
||||||
|
// Enable hot reloading server. It will provide /sockjs-node/ endpoint
|
||||||
|
// for the WebpackDevServer client so it can learn when the files were
|
||||||
|
// updated. The WebpackDevServer client is included as an entry point
|
||||||
|
// in the Webpack development configuration. Note that only changes
|
||||||
|
// to CSS are currently hot reloaded. JS changes will refresh the browser.
|
||||||
|
hot: true,
|
||||||
|
// It is important to tell WebpackDevServer to use the same "root" path
|
||||||
|
// as we specified in the config. In development, we always serve from /.
|
||||||
|
publicPath: '/',
|
||||||
|
// WebpackDevServer is noisy by default so we emit custom message instead
|
||||||
|
// by listening to the compiler events with `compiler.hooks[...].tap` calls above.
|
||||||
|
quiet: true,
|
||||||
|
// Reportedly, this avoids CPU overload on some systems.
|
||||||
|
// https://github.com/facebook/create-react-app/issues/293
|
||||||
|
// src/node_modules is not ignored to support absolute imports
|
||||||
|
// https://github.com/facebook/create-react-app/issues/1065
|
||||||
|
watchOptions: {
|
||||||
|
ignored: ignoredFiles(paths.appSrc),
|
||||||
|
},
|
||||||
|
// Enable HTTPS if the HTTPS environment variable is set to 'true'
|
||||||
|
https: protocol === 'https',
|
||||||
|
host,
|
||||||
|
overlay: false,
|
||||||
|
historyApiFallback: {
|
||||||
|
// Paths with dots should still use the history fallback.
|
||||||
|
// See https://github.com/facebook/create-react-app/issues/387.
|
||||||
|
disableDotRule: true,
|
||||||
|
},
|
||||||
|
public: allowedHost,
|
||||||
|
proxy,
|
||||||
|
before(app, server) {
|
||||||
|
if (fs.existsSync(paths.proxySetup)) {
|
||||||
|
// This registers user provided middleware for proxy reasons
|
||||||
|
require(paths.proxySetup)(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This lets us fetch source contents from webpack for the error overlay
|
||||||
|
app.use(evalSourceMapMiddleware(server));
|
||||||
|
// This lets us open files from the runtime error overlay.
|
||||||
|
app.use(errorOverlayMiddleware());
|
||||||
|
|
||||||
|
// This service worker file is effectively a 'no-op' that will reset any
|
||||||
|
// previous service worker registered for the same host:port combination.
|
||||||
|
// We do this in development to avoid hitting the production cache if
|
||||||
|
// it used the same host and port.
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
|
||||||
|
app.use(noopServiceWorkerMiddleware());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
49
main.js
Normal file
49
main.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// 引入electron并创建一个Browserwindow
|
||||||
|
const {app, BrowserWindow} = require('electron')
|
||||||
|
const path = require('path')
|
||||||
|
const url = require('url')
|
||||||
|
|
||||||
|
// 保持window对象的全局引用,避免JavaScript对象被垃圾回收时,窗口被自动关闭.
|
||||||
|
let mainWindow
|
||||||
|
|
||||||
|
function createWindow () {
|
||||||
|
//创建浏览器窗口,宽高自定义具体大小你开心就好
|
||||||
|
mainWindow = new BrowserWindow({width: 800, height: 600})
|
||||||
|
|
||||||
|
// 加载应用----react 打包
|
||||||
|
mainWindow.loadURL(url.format({
|
||||||
|
pathname: path.join(__dirname, './build/index.html'),
|
||||||
|
protocol: 'file:',
|
||||||
|
slashes: true
|
||||||
|
}))
|
||||||
|
// 加载应用----适用于 react 开发时项目
|
||||||
|
// mainWindow.loadURL('http://localhost:3000/');
|
||||||
|
|
||||||
|
// 打开开发者工具,默认不打开
|
||||||
|
// mainWindow.webContents.openDevTools()
|
||||||
|
|
||||||
|
// 关闭window时触发下列事件.
|
||||||
|
mainWindow.on('closed', function () {
|
||||||
|
mainWindow = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当 Electron 完成初始化并准备创建浏览器窗口时调用此方法
|
||||||
|
app.on('ready', createWindow)
|
||||||
|
|
||||||
|
// 所有窗口关闭时退出应用.
|
||||||
|
app.on('window-all-closed', function () {
|
||||||
|
// macOS中除非用户按下 `Cmd + Q` 显式退出,否则应用与菜单栏始终处于活动状态.
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('activate', function () {
|
||||||
|
// macOS中点击Dock图标时没有已打开的其余应用窗口时,则通常在应用中重建一个窗口
|
||||||
|
if (mainWindow === null) {
|
||||||
|
createWindow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 你可以在这个脚本中续写或者使用require引入独立的js文件.
|
242
package.json
Normal file
242
package.json
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
{
|
||||||
|
"name": "markdown2wechat",
|
||||||
|
"author": "tale",
|
||||||
|
"description": "a markdown editor with the function of style edition",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": false,
|
||||||
|
"main": "lib/Lib.js",
|
||||||
|
"module": "lib/Lib.js",
|
||||||
|
"homepage": "http://md.aizhuanqian.online",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"typings": "./lib/index.d.ts",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/TaleAi/markdown2html"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "node scripts/start.js",
|
||||||
|
"watch": "node ./watch.js",
|
||||||
|
"build": "export GENERATE_SOURCEMAP=false&&node --max_old_space_size=4096 scripts/build.js",
|
||||||
|
"test": "node scripts/test.js",
|
||||||
|
"analyze": "source-map-explorer build/static/js/*.js",
|
||||||
|
"lint": "eslint src --ext ts,tsx,js --fix",
|
||||||
|
"publish:npm": "cross-env NODE_ENV=production && rm -rf lib && mkdir lib && cross-env BABEL_ENV=production npx babel src --out-dir lib --copy-files",
|
||||||
|
"storybook": "npm run publish:npm && start-storybook -p 9001 -c .storybook"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sitdown/juejin": "^1.1.1",
|
||||||
|
"@sitdown/wechat": "^1.1.4",
|
||||||
|
"@sitdown/zhihu": "^1.1.2",
|
||||||
|
"@uiw/react-codemirror": "^1.0.28",
|
||||||
|
"ali-oss": "^6.1.1",
|
||||||
|
"antd": "^3.15.1",
|
||||||
|
"axios": "^0.18.0",
|
||||||
|
"diff-match-patch": "^1.0.4",
|
||||||
|
"highlight.js": "^9.15.6",
|
||||||
|
"juice": "^5.2.0",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
|
"markdown-it": "^8.4.2",
|
||||||
|
"markdown-it-deflist": "^2.0.3",
|
||||||
|
"markdown-it-footnote": "^3.0.1",
|
||||||
|
"markdown-it-implicit-figures": "^0.9.0",
|
||||||
|
"markdown-it-imsize": "^2.0.1",
|
||||||
|
"markdown-it-katex": "^2.0.3",
|
||||||
|
"markdown-it-ruby": "^0.1.1",
|
||||||
|
"markdown-it-table-of-contents": "^0.4.4",
|
||||||
|
"mathjax": "^3.0.1",
|
||||||
|
"mobx": "^5.9.0",
|
||||||
|
"mobx-react": "^5.4.3",
|
||||||
|
"prettier": "^1.19.1",
|
||||||
|
"qiniu-js": "^2.5.4",
|
||||||
|
"react": "16.10.2",
|
||||||
|
"react-dom": "16.10.2",
|
||||||
|
"react-helmet": "^5.2.1",
|
||||||
|
"sitdown": "^1.1.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/cli": "^7.6.2",
|
||||||
|
"@babel/core": "7.2.2",
|
||||||
|
"@storybook/react": "^4.1.11",
|
||||||
|
"@svgr/webpack": "^4.1.0",
|
||||||
|
"babel-core": "7.0.0-bridge.0",
|
||||||
|
"babel-eslint": "^9.0.0",
|
||||||
|
"babel-jest": "23.6.0",
|
||||||
|
"babel-loader": "8.0.5",
|
||||||
|
"babel-plugin-inline-import": "^3.0.0",
|
||||||
|
"babel-plugin-named-asset-import": "^0.3.1",
|
||||||
|
"babel-preset-react-app": "^7.0.1",
|
||||||
|
"bfj": "6.1.1",
|
||||||
|
"case-sensitive-paths-webpack-plugin": "2.2.0",
|
||||||
|
"chalk": "^2.4.2",
|
||||||
|
"chokidar": "^3.2.1",
|
||||||
|
"cross-env": "^6.0.3",
|
||||||
|
"css-loader": "^2.1.1",
|
||||||
|
"dotenv": "6.0.0",
|
||||||
|
"dotenv-expand": "4.2.0",
|
||||||
|
"eslint": "^6.5.0",
|
||||||
|
"eslint-config-airbnb": "^18.0.1",
|
||||||
|
"eslint-config-prettier": "^6.3.0",
|
||||||
|
"eslint-config-react-app": "^3.0.7",
|
||||||
|
"eslint-loader": "^2.1.1",
|
||||||
|
"eslint-plugin-babel": "^5.3.0",
|
||||||
|
"eslint-plugin-flowtype": "2.50.1",
|
||||||
|
"eslint-plugin-import": "^2.14.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.1.2",
|
||||||
|
"eslint-plugin-prettier": "^3.1.1",
|
||||||
|
"eslint-plugin-react": "^7.12.4",
|
||||||
|
"file-loader": "2.0.0",
|
||||||
|
"fork-ts-checker-webpack-plugin-alt": "0.4.14",
|
||||||
|
"fs-extra": "7.0.1",
|
||||||
|
"html-webpack-plugin": "4.0.0-alpha.2",
|
||||||
|
"husky": "^3.0.7",
|
||||||
|
"identity-obj-proxy": "3.0.0",
|
||||||
|
"jest": "23.6.0",
|
||||||
|
"jest-pnp-resolver": "1.0.2",
|
||||||
|
"jest-resolve": "23.6.0",
|
||||||
|
"jest-watch-typeahead": "^0.2.1",
|
||||||
|
"lint-staged": "^9.4.0",
|
||||||
|
"mini-css-extract-plugin": "0.5.0",
|
||||||
|
"optimize-css-assets-webpack-plugin": "5.0.1",
|
||||||
|
"pnp-webpack-plugin": "1.2.1",
|
||||||
|
"postcss-flexbugs-fixes": "4.1.0",
|
||||||
|
"postcss-loader": "3.0.0",
|
||||||
|
"postcss-preset-env": "6.5.0",
|
||||||
|
"postcss-safe-parser": "4.0.1",
|
||||||
|
"pretty-quick": "^1.11.1",
|
||||||
|
"raw-loader": "^4.0.0",
|
||||||
|
"react-app-polyfill": "^0.2.1",
|
||||||
|
"react-dev-utils": "^7.0.3",
|
||||||
|
"resolve": "1.10.0",
|
||||||
|
"sass-loader": "7.1.0",
|
||||||
|
"shelljs": "^0.8.3",
|
||||||
|
"source-map-explorer": "^2.0.1",
|
||||||
|
"style-loader": "0.23.1",
|
||||||
|
"styled-jsx": "^3.2.1",
|
||||||
|
"terser-webpack-plugin": "1.2.2",
|
||||||
|
"thread-loader": "^2.1.3",
|
||||||
|
"to-string-loader": "^1.1.5",
|
||||||
|
"url-loader": "1.1.2",
|
||||||
|
"webpack": "4.28.3",
|
||||||
|
"webpack-dev-server": "3.1.14",
|
||||||
|
"webpack-manifest-plugin": "2.0.4",
|
||||||
|
"workbox-webpack-plugin": "3.6.3"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"productName": "Markdown2Html",
|
||||||
|
"appId": "com.aizhuanqian.www",
|
||||||
|
"mac": {
|
||||||
|
"target": [
|
||||||
|
"dmg",
|
||||||
|
"zip"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": [
|
||||||
|
"nsis",
|
||||||
|
"zip"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"build",
|
||||||
|
"main.js",
|
||||||
|
"package.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "react-app"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not ie <= 11",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"jest": {
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"src/**/*.{js,jsx,ts,tsx}",
|
||||||
|
"!src/**/*.d.ts"
|
||||||
|
],
|
||||||
|
"resolver": "jest-pnp-resolver",
|
||||||
|
"setupFiles": [
|
||||||
|
"react-app-polyfill/jsdom"
|
||||||
|
],
|
||||||
|
"testMatch": [
|
||||||
|
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
|
||||||
|
"<rootDir>/src/**/?(*.)(spec|test).{js,jsx,ts,tsx}"
|
||||||
|
],
|
||||||
|
"testEnvironment": "jsdom",
|
||||||
|
"testURL": "http://localhost",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
|
||||||
|
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
|
||||||
|
"^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
|
||||||
|
},
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$",
|
||||||
|
"^.+\\.module\\.(css|sass|scss)$"
|
||||||
|
],
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^react-native$": "react-native-web",
|
||||||
|
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
|
||||||
|
},
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"web.js",
|
||||||
|
"js",
|
||||||
|
"web.ts",
|
||||||
|
"ts",
|
||||||
|
"web.tsx",
|
||||||
|
"tsx",
|
||||||
|
"json",
|
||||||
|
"web.jsx",
|
||||||
|
"jsx",
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
"watchPlugins": []
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"presets": [
|
||||||
|
"@babel/react"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@babel/plugin-proposal-decorators",
|
||||||
|
{
|
||||||
|
"legacy": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@babel/plugin-proposal-class-properties",
|
||||||
|
{
|
||||||
|
"loose": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"production": {
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"babel-plugin-inline-import",
|
||||||
|
{
|
||||||
|
"extensions": [
|
||||||
|
".md"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "lint-staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"src/**/*.{jsx,txs,ts,js,json}": [
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --fix",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
16
public/favicon.svg
Normal file
16
public/favicon.svg
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" version="1.1" width="400" height="400" style="">
|
||||||
|
<rect width="100%" height="100%" fill="black" rx="20%" ry="20%" />
|
||||||
|
<g fill="#ffffff" transform="translate(300, 100)">
|
||||||
|
<g transform="scale(16)">
|
||||||
|
<svg filter="url(#colors2300178586)" x="0" y="0" width="27.442663140478597" height="50">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17.47 31.83">
|
||||||
|
<g fill="#ffffff">
|
||||||
|
<path d="M0 31.83V0h.81v31.02h16.66v.81H0z"/>
|
||||||
|
<path d="M6.69 26.11V0h3.24v22.87h7.54v3.24H6.69z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</svg>
|
After Width: | Height: | Size: 739 B |
54
public/index.html
Normal file
54
public/index.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta name="keywords" content="markdown,微信公众号,微信公众号编辑器,markdown转微信公众号,微信公众号markdown" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is added to the
|
||||||
|
homescreen on Android. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<title>Markdown2Html</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
<script>
|
||||||
|
var _hmt = _hmt || [];
|
||||||
|
(function () {
|
||||||
|
var hm = document.createElement("script");
|
||||||
|
hm.src = "https://hm.baidu.com/hm.js?897783afa3c6079cf0ac98fe04e7ead7";
|
||||||
|
var s = document.getElementsByTagName("script")[0];
|
||||||
|
s.parentNode.insertBefore(hm, s);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
20
public/manifest.json
Normal file
20
public/manifest.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"short_name": "Markdown Nice",
|
||||||
|
"name": "Markdown Nice",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "https://my-wechat.mdnice.com/mdnice/mdnice%20logo_20191007150129.png",
|
||||||
|
"sizes": "128x128",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://my-wechat.mdnice.com/mdnice/mdnice%20logo_20191007150129.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "./index.html",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
BIN
screenshot.jpg
Normal file
BIN
screenshot.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 127 KiB |
192
scripts/build.js
Normal file
192
scripts/build.js
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Do this as the first thing so that any code reading it knows the right env.
|
||||||
|
process.env.BABEL_ENV = 'production';
|
||||||
|
process.env.NODE_ENV = 'production';
|
||||||
|
|
||||||
|
// Makes the script crash on unhandled rejections instead of silently
|
||||||
|
// ignoring them. In the future, promise rejections that are not handled will
|
||||||
|
// terminate the Node.js process with a non-zero exit code.
|
||||||
|
process.on('unhandledRejection', err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure environment variables are read.
|
||||||
|
require('../config/env');
|
||||||
|
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const chalk = require('react-dev-utils/chalk');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const bfj = require('bfj');
|
||||||
|
const configFactory = require('../config/webpack.config');
|
||||||
|
const paths = require('../config/paths');
|
||||||
|
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||||
|
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
||||||
|
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
|
||||||
|
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
|
||||||
|
const printBuildError = require('react-dev-utils/printBuildError');
|
||||||
|
|
||||||
|
const measureFileSizesBeforeBuild =
|
||||||
|
FileSizeReporter.measureFileSizesBeforeBuild;
|
||||||
|
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
|
||||||
|
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||||
|
|
||||||
|
// These sizes are pretty large. We'll warn for bundles exceeding them.
|
||||||
|
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
|
||||||
|
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
|
||||||
|
|
||||||
|
const isInteractive = process.stdout.isTTY;
|
||||||
|
|
||||||
|
// Warn and crash if required files are missing
|
||||||
|
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process CLI arguments
|
||||||
|
const argv = process.argv.slice(2);
|
||||||
|
const writeStatsJson = argv.indexOf('--stats') !== -1;
|
||||||
|
|
||||||
|
// Generate configuration
|
||||||
|
const config = configFactory('production');
|
||||||
|
|
||||||
|
// We require that you explicitly set browsers and do not fall back to
|
||||||
|
// browserslist defaults.
|
||||||
|
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||||
|
checkBrowsers(paths.appPath, isInteractive)
|
||||||
|
.then(() => {
|
||||||
|
// First, read the current file sizes in build directory.
|
||||||
|
// This lets us display how much they changed later.
|
||||||
|
return measureFileSizesBeforeBuild(paths.appBuild);
|
||||||
|
})
|
||||||
|
.then(previousFileSizes => {
|
||||||
|
// Remove all content but keep the directory so that
|
||||||
|
// if you're in it, you don't end up in Trash
|
||||||
|
fs.emptyDirSync(paths.appBuild);
|
||||||
|
// Merge with the public folder
|
||||||
|
copyPublicFolder();
|
||||||
|
// Start the webpack build
|
||||||
|
return build(previousFileSizes);
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
({ stats, previousFileSizes, warnings }) => {
|
||||||
|
if (warnings.length) {
|
||||||
|
console.log(chalk.yellow('Compiled with warnings.\n'));
|
||||||
|
console.log(warnings.join('\n\n'));
|
||||||
|
console.log(
|
||||||
|
'\nSearch for the ' +
|
||||||
|
chalk.underline(chalk.yellow('keywords')) +
|
||||||
|
' to learn more about each warning.'
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'To ignore, add ' +
|
||||||
|
chalk.cyan('// eslint-disable-next-line') +
|
||||||
|
' to the line before.\n'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(chalk.green('Compiled successfully.\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('File sizes after gzip:\n');
|
||||||
|
printFileSizesAfterBuild(
|
||||||
|
stats,
|
||||||
|
previousFileSizes,
|
||||||
|
paths.appBuild,
|
||||||
|
WARN_AFTER_BUNDLE_GZIP_SIZE,
|
||||||
|
WARN_AFTER_CHUNK_GZIP_SIZE
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
const appPackage = require(paths.appPackageJson);
|
||||||
|
const publicUrl = paths.publicUrl;
|
||||||
|
const publicPath = config.output.publicPath;
|
||||||
|
const buildFolder = path.relative(process.cwd(), paths.appBuild);
|
||||||
|
printHostingInstructions(
|
||||||
|
appPackage,
|
||||||
|
publicUrl,
|
||||||
|
publicPath,
|
||||||
|
buildFolder,
|
||||||
|
useYarn
|
||||||
|
);
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
console.log(chalk.red('Failed to compile.\n'));
|
||||||
|
printBuildError(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch(err => {
|
||||||
|
if (err && err.message) {
|
||||||
|
console.log(err.message);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the production build and print the deployment instructions.
|
||||||
|
function build(previousFileSizes) {
|
||||||
|
console.log('Creating an optimized production build...');
|
||||||
|
|
||||||
|
let compiler = webpack(config);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
compiler.run((err, stats) => {
|
||||||
|
let messages;
|
||||||
|
if (err) {
|
||||||
|
if (!err.message) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
messages = formatWebpackMessages({
|
||||||
|
errors: [err.message],
|
||||||
|
warnings: [],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messages = formatWebpackMessages(
|
||||||
|
stats.toJson({ all: false, warnings: true, errors: true })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (messages.errors.length) {
|
||||||
|
// Only keep the first error. Others are often indicative
|
||||||
|
// of the same problem, but confuse the reader with noise.
|
||||||
|
if (messages.errors.length > 1) {
|
||||||
|
messages.errors.length = 1;
|
||||||
|
}
|
||||||
|
return reject(new Error(messages.errors.join('\n\n')));
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
process.env.CI &&
|
||||||
|
(typeof process.env.CI !== 'string' ||
|
||||||
|
process.env.CI.toLowerCase() !== 'false') &&
|
||||||
|
messages.warnings.length
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
'\nTreating warnings as errors because process.env.CI = true.\n' +
|
||||||
|
'Most CI servers set it automatically.\n'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return reject(new Error(messages.warnings.join('\n\n')));
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveArgs = {
|
||||||
|
stats,
|
||||||
|
previousFileSizes,
|
||||||
|
warnings: messages.warnings,
|
||||||
|
};
|
||||||
|
if (writeStatsJson) {
|
||||||
|
return bfj
|
||||||
|
.write(paths.appBuild + '/bundle-stats.json', stats.toJson())
|
||||||
|
.then(() => resolve(resolveArgs))
|
||||||
|
.catch(error => reject(new Error(error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(resolveArgs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyPublicFolder() {
|
||||||
|
fs.copySync(paths.appPublic, paths.appBuild, {
|
||||||
|
dereference: true,
|
||||||
|
filter: file => file !== paths.appHtml,
|
||||||
|
});
|
||||||
|
}
|
117
scripts/start.js
Normal file
117
scripts/start.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Do this as the first thing so that any code reading it knows the right env.
|
||||||
|
process.env.BABEL_ENV = 'development';
|
||||||
|
process.env.NODE_ENV = 'development';
|
||||||
|
|
||||||
|
// Makes the script crash on unhandled rejections instead of silently
|
||||||
|
// ignoring them. In the future, promise rejections that are not handled will
|
||||||
|
// terminate the Node.js process with a non-zero exit code.
|
||||||
|
process.on('unhandledRejection', err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure environment variables are read.
|
||||||
|
require('../config/env');
|
||||||
|
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const chalk = require('react-dev-utils/chalk');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const WebpackDevServer = require('webpack-dev-server');
|
||||||
|
const clearConsole = require('react-dev-utils/clearConsole');
|
||||||
|
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||||
|
const {
|
||||||
|
choosePort,
|
||||||
|
createCompiler,
|
||||||
|
prepareProxy,
|
||||||
|
prepareUrls,
|
||||||
|
} = require('react-dev-utils/WebpackDevServerUtils');
|
||||||
|
const openBrowser = require('react-dev-utils/openBrowser');
|
||||||
|
const paths = require('../config/paths');
|
||||||
|
const configFactory = require('../config/webpack.config');
|
||||||
|
const createDevServerConfig = require('../config/webpackDevServer.config');
|
||||||
|
|
||||||
|
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||||
|
const isInteractive = process.stdout.isTTY;
|
||||||
|
|
||||||
|
// Warn and crash if required files are missing
|
||||||
|
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tools like Cloud9 rely on this.
|
||||||
|
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
|
||||||
|
const HOST = process.env.HOST || '0.0.0.0';
|
||||||
|
|
||||||
|
if (process.env.HOST) {
|
||||||
|
console.log(
|
||||||
|
chalk.cyan(
|
||||||
|
`Attempting to bind to HOST environment variable: ${chalk.yellow(
|
||||||
|
chalk.bold(process.env.HOST)
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`If this was unintentional, check that you haven't mistakenly set it in your shell.`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`Learn more here: ${chalk.yellow('http://bit.ly/CRA-advanced-config')}`
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We require that you explictly set browsers and do not fall back to
|
||||||
|
// browserslist defaults.
|
||||||
|
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||||
|
checkBrowsers(paths.appPath, isInteractive)
|
||||||
|
.then(() => {
|
||||||
|
// We attempt to use the default port but if it is busy, we offer the user to
|
||||||
|
// run on a different port. `choosePort()` Promise resolves to the next free port.
|
||||||
|
return choosePort(HOST, DEFAULT_PORT);
|
||||||
|
})
|
||||||
|
.then(port => {
|
||||||
|
if (port == null) {
|
||||||
|
// We have not found a port.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const config = configFactory('development');
|
||||||
|
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||||
|
const appName = require(paths.appPackageJson).name;
|
||||||
|
const urls = prepareUrls(protocol, HOST, port);
|
||||||
|
// Create a webpack compiler that is configured with custom messages.
|
||||||
|
const compiler = createCompiler(webpack, config, appName, urls, useYarn);
|
||||||
|
// Load proxy config
|
||||||
|
const proxySetting = require(paths.appPackageJson).proxy;
|
||||||
|
const proxyConfig = prepareProxy(proxySetting, paths.appPublic);
|
||||||
|
// Serve webpack assets generated by the compiler over a web server.
|
||||||
|
const serverConfig = createDevServerConfig(
|
||||||
|
proxyConfig,
|
||||||
|
urls.lanUrlForConfig
|
||||||
|
);
|
||||||
|
const devServer = new WebpackDevServer(compiler, serverConfig);
|
||||||
|
// Launch WebpackDevServer.
|
||||||
|
devServer.listen(port, HOST, err => {
|
||||||
|
if (err) {
|
||||||
|
return console.log(err);
|
||||||
|
}
|
||||||
|
if (isInteractive) {
|
||||||
|
clearConsole();
|
||||||
|
}
|
||||||
|
console.log(chalk.cyan('Starting the development server...\n'));
|
||||||
|
openBrowser(urls.localUrlForBrowser);
|
||||||
|
});
|
||||||
|
|
||||||
|
['SIGINT', 'SIGTERM'].forEach(function(sig) {
|
||||||
|
process.on(sig, function() {
|
||||||
|
devServer.close();
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err && err.message) {
|
||||||
|
console.log(err.message);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
});
|
60
scripts/test.js
Normal file
60
scripts/test.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Do this as the first thing so that any code reading it knows the right env.
|
||||||
|
process.env.BABEL_ENV = 'test';
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
process.env.PUBLIC_URL = '';
|
||||||
|
|
||||||
|
// Makes the script crash on unhandled rejections instead of silently
|
||||||
|
// ignoring them. In the future, promise rejections that are not handled will
|
||||||
|
// terminate the Node.js process with a non-zero exit code.
|
||||||
|
process.on('unhandledRejection', err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure environment variables are read.
|
||||||
|
require('../config/env');
|
||||||
|
|
||||||
|
|
||||||
|
const jest = require('jest');
|
||||||
|
const execSync = require('child_process').execSync;
|
||||||
|
let argv = process.argv.slice(2);
|
||||||
|
|
||||||
|
function isInGitRepository() {
|
||||||
|
try {
|
||||||
|
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInMercurialRepository() {
|
||||||
|
try {
|
||||||
|
execSync('hg --cwd . root', { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch unless on CI, in coverage mode, explicitly adding `--no-watch`,
|
||||||
|
// or explicitly running all tests
|
||||||
|
if (
|
||||||
|
!process.env.CI &&
|
||||||
|
argv.indexOf('--coverage') === -1 &&
|
||||||
|
argv.indexOf('--no-watch') === -1 &&
|
||||||
|
argv.indexOf('--watchAll') === -1
|
||||||
|
) {
|
||||||
|
// https://github.com/facebook/create-react-app/issues/5210
|
||||||
|
const hasSourceControl = isInGitRepository() || isInMercurialRepository();
|
||||||
|
argv.push(hasSourceControl ? '--watch' : '--watchAll');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jest doesn't have this option so we'll remove it
|
||||||
|
if (argv.indexOf('--no-watch') !== -1) {
|
||||||
|
argv = argv.filter(arg => arg !== '--no-watch');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
jest.run(argv);
|
210
src/App.css
Normal file
210
src/App.css
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
.ant-btn-primary {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #000;
|
||||||
|
border-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn-primary:hover,
|
||||||
|
.ant-btn-primary:focus {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #000;
|
||||||
|
border-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn:hover, .ant-btn:focus {
|
||||||
|
color: #000;
|
||||||
|
background-color: #fff;
|
||||||
|
border-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input:hover, .ant-input:focus {
|
||||||
|
border-color: #000;
|
||||||
|
border-right-width: 1px !important;
|
||||||
|
-webkit-box-shadow: 0 0 0 2px rgb(82 82 83 / 20%);
|
||||||
|
box-shadow: 0 0 0 2px rgb(82 82 83 / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-selection:hover {
|
||||||
|
border-color: #000;
|
||||||
|
border-right-width: 1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-focused .ant-select-selection, .ant-select-selection:focus, .ant-select-selection:active,
|
||||||
|
.ant-input-number:focus, .ant-input-number:active , .ant-input-number:hover {
|
||||||
|
border-color: #000;
|
||||||
|
-webkit-box-shadow: 0 0 0 2px rgb(82 82 83 / 20%);
|
||||||
|
box-shadow: 0 0 0 2px rgb(82 82 83 / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled) {
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-dropdown-menu {
|
||||||
|
margin: 4px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-dropdown-menu-item {
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-item-selected {
|
||||||
|
color: #000;
|
||||||
|
/*font-weight: bolder;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-item {
|
||||||
|
text-align: center;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-inline, .ant-menu-vertical, .ant-menu-vertical-left {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-item:hover, .ant-menu-item-active, .ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open, .ant-menu-submenu-active, .ant-menu-submenu-title:hover {
|
||||||
|
color: #000;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-app {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: "PingFang SC", BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serifhtml, body;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-text-container {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - 50px);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-text-container-immersive {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-md-editing-immersive {
|
||||||
|
padding: 0px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-md-editing-immersive .CodeMirror-lines {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.nice-md-editing-immersive .CodeMirror-lines {
|
||||||
|
padding: 20px 10%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1024px) {
|
||||||
|
.nice-md-editing-immersive .CodeMirror-lines {
|
||||||
|
padding: 20px 15%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 编辑器最多会被分成三份width:33.3%,当两份时根据flex-grow:1伸展 */
|
||||||
|
.nice-md-editing,
|
||||||
|
.nice-style-editing {
|
||||||
|
position: relative;
|
||||||
|
width: 33.3%;
|
||||||
|
height: 88%;
|
||||||
|
flex-grow: 1;
|
||||||
|
word-wrap: break-word;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-marked-text {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 33.3%;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 70px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-marked-text-pc {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-wx-box {
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 25px 20px;
|
||||||
|
height: 98%;
|
||||||
|
width: 375px;
|
||||||
|
box-shadow: 0 0 60px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-wx-box-pc {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px 35px 20px 20px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-style-editing-hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-md-editing-hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-marked-text-hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(0, 0, 0, 0.12);
|
||||||
|
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.nice-md-editing {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.nice-navbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.nice-sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.nice-wx-box {
|
||||||
|
overflow: visible;
|
||||||
|
box-shadow: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.nice-style-editing {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#nice-rich-text {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.nice-footer-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
410
src/App.js
Normal file
410
src/App.js
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import CodeMirror from "@uiw/react-codemirror";
|
||||||
|
import "codemirror/addon/search/searchcursor";
|
||||||
|
import "codemirror/keymap/sublime";
|
||||||
|
import "antd/dist/antd.css";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import throttle from "lodash.throttle";
|
||||||
|
|
||||||
|
import Dialog from "./layout/Dialog";
|
||||||
|
import Navbar from "./layout/Navbar";
|
||||||
|
import Toobar from "./layout/Toolbar";
|
||||||
|
import Footer from "./layout/Footer";
|
||||||
|
import Sidebar from "./layout/Sidebar";
|
||||||
|
import StyleEditor from "./layout/StyleEditor";
|
||||||
|
import EditorMenu from "./layout/EditorMenu";
|
||||||
|
import SearchBox from "./component/SearchBox";
|
||||||
|
|
||||||
|
import "./App.css";
|
||||||
|
import "./utils/mdMirror.css";
|
||||||
|
|
||||||
|
import {
|
||||||
|
LAYOUT_ID,
|
||||||
|
BOX_ID,
|
||||||
|
IMAGE_HOSTING_NAMES,
|
||||||
|
IMAGE_HOSTING_TYPE,
|
||||||
|
MJX_DATA_FORMULA,
|
||||||
|
MJX_DATA_FORMULA_TYPE,
|
||||||
|
} from "./utils/constant";
|
||||||
|
import {markdownParser, markdownParserWechat, updateMathjax} from "./utils/helper";
|
||||||
|
import pluginCenter from "./utils/pluginCenter";
|
||||||
|
import appContext from "./utils/appContext";
|
||||||
|
import {uploadAdaptor} from "./utils/imageHosting";
|
||||||
|
import bindHotkeys, {betterTab, rightClick} from "./utils/hotkey";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@inject("navbar")
|
||||||
|
@inject("footer")
|
||||||
|
@inject("view")
|
||||||
|
@inject("dialog")
|
||||||
|
@inject("imageHosting")
|
||||||
|
@observer
|
||||||
|
class App extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.scale = 1;
|
||||||
|
this.handleUpdateMathjax = throttle(updateMathjax, 1500);
|
||||||
|
this.state = {
|
||||||
|
focus: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener("fullscreenchange", this.solveScreenChange);
|
||||||
|
document.addEventListener("webkitfullscreenchange", this.solveScreenChange);
|
||||||
|
document.addEventListener("mozfullscreenchange", this.solveScreenChange);
|
||||||
|
document.addEventListener("MSFullscreenChange", this.solveScreenChange);
|
||||||
|
try {
|
||||||
|
window.MathJax = {
|
||||||
|
tex: {
|
||||||
|
inlineMath: [["$", "$"]],
|
||||||
|
displayMath: [["$$", "$$"]],
|
||||||
|
tags: "ams",
|
||||||
|
},
|
||||||
|
svg: {
|
||||||
|
fontCache: "none",
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
renderActions: {
|
||||||
|
addMenu: [0, "", ""],
|
||||||
|
addContainer: [
|
||||||
|
190,
|
||||||
|
(doc) => {
|
||||||
|
for (const math of doc.math) {
|
||||||
|
this.addContainer(math, doc);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
this.addContainer,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line
|
||||||
|
require("mathjax/es5/tex-svg-full");
|
||||||
|
pluginCenter.mathjax = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
this.setEditorContent();
|
||||||
|
this.setCustomImageHosting();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
if (pluginCenter.mathjax) {
|
||||||
|
this.handleUpdateMathjax();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener("fullscreenchange", this.solveScreenChange);
|
||||||
|
document.removeEventListener("webkitfullscreenchange", this.solveScreenChange);
|
||||||
|
document.removeEventListener("mozfullscreenchange", this.solveScreenChange);
|
||||||
|
document.removeEventListener("MSFullscreenChange", this.solveScreenChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCustomImageHosting = () => {
|
||||||
|
if (this.props.useImageHosting === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {url, name, isSmmsOpen, isQiniuyunOpen, isAliyunOpen, isGiteeOpen, isGitHubOpen} = this.props.useImageHosting;
|
||||||
|
if (name) {
|
||||||
|
this.props.imageHosting.setHostingUrl(url);
|
||||||
|
this.props.imageHosting.setHostingName(name);
|
||||||
|
this.props.imageHosting.addImageHosting(name);
|
||||||
|
}
|
||||||
|
if (isSmmsOpen) {
|
||||||
|
this.props.imageHosting.addImageHosting(IMAGE_HOSTING_NAMES.smms);
|
||||||
|
}
|
||||||
|
if (isAliyunOpen) {
|
||||||
|
this.props.imageHosting.addImageHosting(IMAGE_HOSTING_NAMES.aliyun);
|
||||||
|
}
|
||||||
|
if (isQiniuyunOpen) {
|
||||||
|
this.props.imageHosting.addImageHosting(IMAGE_HOSTING_NAMES.qiniuyun);
|
||||||
|
}
|
||||||
|
if (isGiteeOpen) {
|
||||||
|
this.props.imageHosting.addImageHosting(IMAGE_HOSTING_NAMES.gitee);
|
||||||
|
}
|
||||||
|
if (isGitHubOpen) {
|
||||||
|
this.props.imageHosting.addImageHosting(IMAGE_HOSTING_NAMES.github);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一次进入没有默认图床时
|
||||||
|
if (window.localStorage.getItem(IMAGE_HOSTING_TYPE) === null) {
|
||||||
|
let type;
|
||||||
|
if (name) {
|
||||||
|
type = name;
|
||||||
|
} else if (isSmmsOpen) {
|
||||||
|
type = IMAGE_HOSTING_NAMES.smms;
|
||||||
|
} else if (isAliyunOpen) {
|
||||||
|
type = IMAGE_HOSTING_NAMES.aliyun;
|
||||||
|
} else if (isQiniuyunOpen) {
|
||||||
|
type = IMAGE_HOSTING_NAMES.qiniuyun;
|
||||||
|
} else if (isGiteeOpen) {
|
||||||
|
type = IMAGE_HOSTING_NAMES.isGitee;
|
||||||
|
}
|
||||||
|
this.props.imageHosting.setType(type);
|
||||||
|
window.localStorage.setItem(IMAGE_HOSTING_TYPE, type);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setEditorContent = () => {
|
||||||
|
const {defaultText} = this.props;
|
||||||
|
if (defaultText) {
|
||||||
|
this.props.content.setContent(defaultText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrentIndex(index) {
|
||||||
|
this.index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
solveScreenChange = () => {
|
||||||
|
const {isImmersiveEditing} = this.props.view;
|
||||||
|
this.props.view.setImmersiveEditing(!isImmersiveEditing);
|
||||||
|
};
|
||||||
|
|
||||||
|
getInstance = (instance) => {
|
||||||
|
instance.editor.on("inputRead", function(cm, event) {
|
||||||
|
if (event.origin === "paste") {
|
||||||
|
var text = event.text[0]; // pasted string
|
||||||
|
var new_text = ""; // any operations here
|
||||||
|
cm.refresh();
|
||||||
|
const {length} = cm.getSelections();
|
||||||
|
// my first idea was
|
||||||
|
// note: for multiline strings may need more complex calculations
|
||||||
|
cm.replaceRange(new_text, event.from, {line: event.from.line, ch: event.from.ch + text.length});
|
||||||
|
// first solution did'nt work (before i guess to call refresh) so i tried that way, works too
|
||||||
|
if (length === 1) {
|
||||||
|
cm.execCommand("undo");
|
||||||
|
}
|
||||||
|
// cm.setCursor(event.from);
|
||||||
|
cm.replaceSelection(new_text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (instance) {
|
||||||
|
this.props.content.setMarkdownEditor(instance.editor);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleScroll = () => {
|
||||||
|
if (this.props.navbar.isSyncScroll) {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const cmData = markdownEditor.getScrollInfo();
|
||||||
|
const editorToTop = cmData.top;
|
||||||
|
const editorScrollHeight = cmData.height - cmData.clientHeight;
|
||||||
|
this.scale = (this.previewWrap.offsetHeight - this.previewContainer.offsetHeight + 55) / editorScrollHeight;
|
||||||
|
if (this.index === 1) {
|
||||||
|
this.previewContainer.scrollTop = editorToTop * this.scale;
|
||||||
|
} else {
|
||||||
|
this.editorTop = this.previewContainer.scrollTop / this.scale;
|
||||||
|
markdownEditor.scrollTo(null, this.editorTop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleChange = (editor) => {
|
||||||
|
if (this.state.focus) {
|
||||||
|
const content = editor.getValue();
|
||||||
|
this.props.content.setContent(content);
|
||||||
|
this.props.onTextChange && this.props.onTextChange(content);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleFocus = (editor) => {
|
||||||
|
this.setState({
|
||||||
|
focus: true,
|
||||||
|
});
|
||||||
|
this.props.onTextFocus && this.props.onTextFocus(editor.getValue());
|
||||||
|
};
|
||||||
|
|
||||||
|
handleBlur = (editor) => {
|
||||||
|
this.setState({
|
||||||
|
focus: false,
|
||||||
|
});
|
||||||
|
this.props.onTextBlur && this.props.onTextBlur(editor.getValue());
|
||||||
|
};
|
||||||
|
|
||||||
|
getStyleInstance = (instance) => {
|
||||||
|
if (instance) {
|
||||||
|
this.styleEditor = instance.editor;
|
||||||
|
this.styleEditor.on("keyup", (cm, e) => {
|
||||||
|
if ((e.keyCode >= 65 && e.keyCode <= 90) || e.keyCode === 189) {
|
||||||
|
cm.showHint(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDrop = (instance, e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// console.log(e.dataTransfer.files[0]);
|
||||||
|
if (!(e.dataTransfer && e.dataTransfer.files)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < e.dataTransfer.files.length; i++) {
|
||||||
|
// console.log(e.dataTransfer.files[i]);
|
||||||
|
uploadAdaptor({file: e.dataTransfer.files[i], content: this.props.content});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handlePaste = (instance, e) => {
|
||||||
|
const cbData = e.clipboardData;
|
||||||
|
|
||||||
|
const insertPasteContent = (cm, content) => {
|
||||||
|
const {length} = cm.getSelections();
|
||||||
|
cm.replaceSelections(Array(length).fill(content));
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
focus: true,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.handleChange(cm);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (e.clipboardData && e.clipboardData.files) {
|
||||||
|
for (let i = 0; i < e.clipboardData.files.length; i++) {
|
||||||
|
uploadAdaptor({file: e.clipboardData.files[i], content: this.props.content});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cbData) {
|
||||||
|
const html = cbData.getData("text/html");
|
||||||
|
const text = cbData.getData("TEXT");
|
||||||
|
insertPasteContent(instance, text);
|
||||||
|
console.log(html);
|
||||||
|
|
||||||
|
if (html) {
|
||||||
|
console.log("htmsdkskdkskdk");
|
||||||
|
this.props.footer.setPasteHtmlChecked(true);
|
||||||
|
this.props.footer.setPasteHtml(html);
|
||||||
|
this.props.footer.setPasteText(text);
|
||||||
|
} else {
|
||||||
|
this.props.footer.setPasteHtmlChecked(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addContainer(math, doc) {
|
||||||
|
const tag = "span";
|
||||||
|
const spanClass = math.display ? "span-block-equation" : "span-inline-equation";
|
||||||
|
const cls = math.display ? "block-equation" : "inline-equation";
|
||||||
|
math.typesetRoot.className = cls;
|
||||||
|
math.typesetRoot.setAttribute(MJX_DATA_FORMULA, math.math);
|
||||||
|
math.typesetRoot.setAttribute(MJX_DATA_FORMULA_TYPE, cls);
|
||||||
|
math.typesetRoot = doc.adaptor.node(tag, {class: spanClass, style: "cursor:pointer"}, [math.typesetRoot]);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {codeNum, previewType} = this.props.navbar;
|
||||||
|
const {isEditAreaOpen, isPreviewAreaOpen, isStyleEditorOpen, isImmersiveEditing} = this.props.view;
|
||||||
|
const {isSearchOpen} = this.props.dialog;
|
||||||
|
|
||||||
|
const parseHtml =
|
||||||
|
codeNum === 0
|
||||||
|
? markdownParserWechat.render(this.props.content.content)
|
||||||
|
: markdownParser.render(this.props.content.content);
|
||||||
|
|
||||||
|
const mdEditingClass = classnames({
|
||||||
|
"nice-md-editing": !isImmersiveEditing,
|
||||||
|
"nice-md-editing-immersive": isImmersiveEditing,
|
||||||
|
"nice-md-editing-hide": !isEditAreaOpen,
|
||||||
|
});
|
||||||
|
|
||||||
|
const styleEditingClass = classnames({
|
||||||
|
"nice-style-editing": true,
|
||||||
|
"nice-style-editing-hide": isImmersiveEditing,
|
||||||
|
});
|
||||||
|
|
||||||
|
const richTextClass = classnames({
|
||||||
|
"nice-marked-text": true,
|
||||||
|
"nice-marked-text-pc": previewType === "pc",
|
||||||
|
"nice-marked-text-hide": isImmersiveEditing || !isPreviewAreaOpen,
|
||||||
|
});
|
||||||
|
|
||||||
|
const richTextBoxClass = classnames({
|
||||||
|
"nice-wx-box": true,
|
||||||
|
"nice-wx-box-pc": previewType === "pc",
|
||||||
|
});
|
||||||
|
|
||||||
|
const textContainerClass = classnames({
|
||||||
|
"nice-text-container": !isImmersiveEditing,
|
||||||
|
"nice-text-container-immersive": isImmersiveEditing,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<appContext.Consumer>
|
||||||
|
{({defaultTitle, onStyleChange, onStyleBlur, onStyleFocus, token}) => (
|
||||||
|
<div className="nice-app">
|
||||||
|
<Navbar title={defaultTitle} token={token} />
|
||||||
|
<Toobar token={token} />
|
||||||
|
<div className={textContainerClass}>
|
||||||
|
<div id="nice-md-editor" className={mdEditingClass} onMouseOver={(e) => this.setCurrentIndex(1, e)}>
|
||||||
|
{isSearchOpen && <SearchBox />}
|
||||||
|
<CodeMirror
|
||||||
|
value={this.props.content.content}
|
||||||
|
options={{
|
||||||
|
theme: "md-mirror",
|
||||||
|
keyMap: "sublime",
|
||||||
|
mode: "markdown",
|
||||||
|
lineWrapping: true,
|
||||||
|
lineNumbers: false,
|
||||||
|
extraKeys: {
|
||||||
|
...bindHotkeys(this.props.content, this.props.dialog),
|
||||||
|
Tab: betterTab,
|
||||||
|
RightClick: rightClick,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
onScroll={this.handleScroll}
|
||||||
|
onFocus={this.handleFocus}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
onDrop={this.handleDrop}
|
||||||
|
onPaste={this.handlePaste}
|
||||||
|
ref={this.getInstance}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div id="nice-rich-text" className={richTextClass} onMouseOver={(e) => this.setCurrentIndex(2, e)}>
|
||||||
|
<Sidebar />
|
||||||
|
<div
|
||||||
|
id={BOX_ID}
|
||||||
|
className={richTextBoxClass}
|
||||||
|
onScroll={this.handleScroll}
|
||||||
|
ref={(node) => {
|
||||||
|
this.previewContainer = node;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
id={LAYOUT_ID}
|
||||||
|
data-tool="markdown2wechat编辑器"
|
||||||
|
data-website="https://aizhuanqian.com"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: parseHtml,
|
||||||
|
}}
|
||||||
|
ref={(node) => {
|
||||||
|
this.previewWrap = node;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isStyleEditorOpen && (
|
||||||
|
<div id="nice-style-editor" className={styleEditingClass}>
|
||||||
|
<StyleEditor onStyleChange={onStyleChange} onStyleBlur={onStyleBlur} onStyleFocus={onStyleFocus} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Dialog />
|
||||||
|
<EditorMenu />
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</appContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
9
src/App.test.js
Normal file
9
src/App.test.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
ReactDOM.render(<App />, div);
|
||||||
|
ReactDOM.unmountComponentAtNode(div);
|
||||||
|
});
|
155
src/Lib.js
Normal file
155
src/Lib.js
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import {Result} from "antd";
|
||||||
|
import {Provider} from "mobx-react";
|
||||||
|
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
import content from "./store/content";
|
||||||
|
import userInfo from "./store/userInfo";
|
||||||
|
import navbar from "./store/navbar";
|
||||||
|
import footer from "./store/footer";
|
||||||
|
import dialog from "./store/dialog";
|
||||||
|
import imageHosting from "./store/imageHosting";
|
||||||
|
import view from "./store/view";
|
||||||
|
|
||||||
|
import {isPC} from "./utils/helper";
|
||||||
|
import appContext from "./utils/appContext";
|
||||||
|
import SvgIcon from "./icon";
|
||||||
|
import {solveWeChatMath, solveZhihuMath, solveHtml} from "./utils/converter";
|
||||||
|
import {LAYOUT_ID} from "./utils/constant";
|
||||||
|
|
||||||
|
class Lib extends Component {
|
||||||
|
getWeChatHtml() {
|
||||||
|
const layout = document.getElementById(LAYOUT_ID); // 保护现场
|
||||||
|
const html = layout.innerHTML;
|
||||||
|
solveWeChatMath();
|
||||||
|
const res = solveHtml();
|
||||||
|
layout.innerHTML = html; // 恢复现场
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
getZhihuHtml() {
|
||||||
|
const layout = document.getElementById(LAYOUT_ID); // 保护现场
|
||||||
|
const html = layout.innerHTML;
|
||||||
|
solveZhihuMath();
|
||||||
|
const res = solveHtml();
|
||||||
|
layout.innerHTML = html; // 恢复现场
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
defaultTitle,
|
||||||
|
defaultText,
|
||||||
|
onTextChange,
|
||||||
|
onTextBlur,
|
||||||
|
onTextFocus,
|
||||||
|
onStyleChange,
|
||||||
|
onStyleBlur,
|
||||||
|
onStyleFocus,
|
||||||
|
token,
|
||||||
|
useImageHosting,
|
||||||
|
} = this.props;
|
||||||
|
const appCtx = {
|
||||||
|
defaultTitle,
|
||||||
|
defaultText,
|
||||||
|
onTextChange,
|
||||||
|
onTextBlur,
|
||||||
|
onTextFocus,
|
||||||
|
onStyleChange,
|
||||||
|
onStyleBlur,
|
||||||
|
onStyleFocus,
|
||||||
|
token,
|
||||||
|
useImageHosting,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Provider
|
||||||
|
content={content}
|
||||||
|
userInfo={userInfo}
|
||||||
|
navbar={navbar}
|
||||||
|
footer={footer}
|
||||||
|
dialog={dialog}
|
||||||
|
imageHosting={imageHosting}
|
||||||
|
view={view}
|
||||||
|
>
|
||||||
|
{isPC() ? (
|
||||||
|
<appContext.Provider value={appCtx}>
|
||||||
|
<App
|
||||||
|
defaultText={defaultText}
|
||||||
|
onTextChange={onTextChange}
|
||||||
|
onTextBlur={onTextBlur}
|
||||||
|
onTextFocus={onTextFocus}
|
||||||
|
onStyleChange={onStyleChange}
|
||||||
|
onStyleBlur={onStyleBlur}
|
||||||
|
onStyleFocus={onStyleFocus}
|
||||||
|
useImageHosting={useImageHosting}
|
||||||
|
token={token}
|
||||||
|
/>
|
||||||
|
</appContext.Provider>
|
||||||
|
) : (
|
||||||
|
<Result
|
||||||
|
icon={<SvgIcon name="smile" style={style.svgIcon} />}
|
||||||
|
title="请使用 PC 端打开排版工具"
|
||||||
|
subTitle="更多 Markdown Nice 信息,请扫码关注公众号「编程如画」"
|
||||||
|
extra={<img alt="" style={{width: "100%"}} src="https://my-wechat.mdnice.com/wechat.jpg" />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
svgIcon: {
|
||||||
|
width: "72px",
|
||||||
|
height: "72px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Lib.defaultProps = {
|
||||||
|
defaultTitle: "",
|
||||||
|
defaultText: "",
|
||||||
|
onTextChange: () => {},
|
||||||
|
onTextBlur: () => {},
|
||||||
|
onTextFocus: () => {},
|
||||||
|
onStyleChange: () => {},
|
||||||
|
onStyleBlur: () => {},
|
||||||
|
onStyleFocus: () => {},
|
||||||
|
token: "",
|
||||||
|
// eslint-disable-next-line react/default-props-match-prop-types
|
||||||
|
useImageHosting: {
|
||||||
|
url: "",
|
||||||
|
name: "",
|
||||||
|
isSmmsOpen: true,
|
||||||
|
isQiniuyunOpen: true,
|
||||||
|
isAliyunOpen: true,
|
||||||
|
isGiteeOpen: true,
|
||||||
|
isGitHubOpen: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Lib.propTypes = {
|
||||||
|
defaultTitle: PropTypes.string,
|
||||||
|
defaultText: PropTypes.string,
|
||||||
|
onTextChange: PropTypes.func,
|
||||||
|
onTextBlur: PropTypes.func,
|
||||||
|
onTextFocus: PropTypes.func,
|
||||||
|
onStyleChange: PropTypes.func,
|
||||||
|
onStyleBlur: PropTypes.func,
|
||||||
|
onStyleFocus: PropTypes.func,
|
||||||
|
token: PropTypes.string,
|
||||||
|
// eslint-disable-next-line react/require-default-props
|
||||||
|
useImageHosting: PropTypes.shape({
|
||||||
|
url: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
isSmmsOpen: PropTypes.bool,
|
||||||
|
isQiniuyunOpen: PropTypes.bool,
|
||||||
|
isAliyunOpen: PropTypes.bool,
|
||||||
|
isGiteeOpen: PropTypes.bool,
|
||||||
|
isGitHubOpen: PropTypes.bool,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Lib;
|
100
src/component/Dialog/AboutDialog.js
Normal file
100
src/component/Dialog/AboutDialog.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {Modal, Button} from "antd";
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class AboutDialog extends Component {
|
||||||
|
handleOk = () => {
|
||||||
|
this.props.dialog.setAboutOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCancel = () => {
|
||||||
|
this.props.dialog.setAboutOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleVersion = () => {
|
||||||
|
this.props.dialog.setAboutOpen(false);
|
||||||
|
this.props.dialog.setVersionOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="关于"
|
||||||
|
okText="确认"
|
||||||
|
cancelText="取消"
|
||||||
|
visible={this.props.dialog.isAboutOpen}
|
||||||
|
onOk={this.handleOk}
|
||||||
|
onCancel={this.handleCancel}
|
||||||
|
footer={[
|
||||||
|
<Button key="submit" type="primary" onClick={this.handleOk}>
|
||||||
|
确认
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
bodyStyle={{
|
||||||
|
paddingTop: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3 style={style.headerMargin}>
|
||||||
|
Markdown2Html
|
||||||
|
<a
|
||||||
|
id="nice-about-dialog-star"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/shenweiyan/Markdown2Html"
|
||||||
|
style={style.noBorder}
|
||||||
|
>
|
||||||
|
<img alt="" style={style.img} src="https://badgen.net/github/stars/shenweiyan/Markdown2Html" />
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p style={style.lineHeight}>支持自定义样式的 Markdown 编辑器;</p>
|
||||||
|
<p style={style.lineHeight}>支持微信公众号、知乎和稀土掘金;</p>
|
||||||
|
<p style={style.lineHeight}>
|
||||||
|
如果你喜欢我们的工具,欢迎关注
|
||||||
|
<a
|
||||||
|
id="nice-about-dialog-github"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/shenweiyan/Markdown2Html"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
leftImgWidth: {
|
||||||
|
width: "40%",
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
rightImgWidth: {
|
||||||
|
width: "60%",
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
headerMargin: {
|
||||||
|
marginTop: "5px",
|
||||||
|
marginBottom: "5px",
|
||||||
|
color: "black",
|
||||||
|
},
|
||||||
|
lineHeight: {
|
||||||
|
lineHeight: "26px",
|
||||||
|
color: "black",
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
img: {
|
||||||
|
width: "70px",
|
||||||
|
marginLeft: "10px",
|
||||||
|
display: "inline-block",
|
||||||
|
},
|
||||||
|
noBorder: {
|
||||||
|
border: "none",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AboutDialog;
|
102
src/component/Dialog/FormDialog.js
Normal file
102
src/component/Dialog/FormDialog.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {Modal, InputNumber, Form} from "antd";
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@inject("content")
|
||||||
|
@observer
|
||||||
|
class FormDialog extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
...initialState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
buildRow = (rowNum, columnNum) => {
|
||||||
|
let appendText = "|";
|
||||||
|
if (rowNum === 1) {
|
||||||
|
appendText += " --- |";
|
||||||
|
for (let i = 0; i < columnNum - 1; i++) {
|
||||||
|
appendText += " --- |";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appendText += " |";
|
||||||
|
for (let i = 0; i < columnNum - 1; i++) {
|
||||||
|
appendText += " |";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return appendText + (/windows|win32/i.test(navigator.userAgent) ? "\r\n" : "\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
buildFormFormat = (rowNum, columnNum) => {
|
||||||
|
let formFormat = "";
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
formFormat += this.buildRow(i, columnNum);
|
||||||
|
}
|
||||||
|
for (let i = 3; i <= rowNum; i++) {
|
||||||
|
formFormat += this.buildRow(i, columnNum);
|
||||||
|
}
|
||||||
|
return formFormat;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleOk = () => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const cursor = markdownEditor.getCursor();
|
||||||
|
|
||||||
|
const text = this.buildFormFormat(this.state.rowNum, this.state.columnNum);
|
||||||
|
markdownEditor.replaceSelection(text, cursor);
|
||||||
|
|
||||||
|
const content = markdownEditor.getValue();
|
||||||
|
this.props.content.setContent(content);
|
||||||
|
|
||||||
|
this.handleCancel();
|
||||||
|
cursor.ch += 2;
|
||||||
|
markdownEditor.setCursor(cursor);
|
||||||
|
markdownEditor.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCancel = () => {
|
||||||
|
this.setState(initialState);
|
||||||
|
this.props.dialog.setFormOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="添加表格"
|
||||||
|
okText="确认"
|
||||||
|
cancelText="取消"
|
||||||
|
visible={this.props.dialog.isFormOpen}
|
||||||
|
onOk={this.handleOk}
|
||||||
|
onCancel={this.handleCancel}
|
||||||
|
>
|
||||||
|
<Form.Item label="行数" labelCol={{span: 4}}>
|
||||||
|
<InputNumber
|
||||||
|
min={2}
|
||||||
|
max={10}
|
||||||
|
value={this.state.rowNum}
|
||||||
|
defaultValue={1}
|
||||||
|
onChange={(value) => this.setState({rowNum: value})}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="列数" labelCol={{span: 4}}>
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
max={10}
|
||||||
|
value={this.state.columnNum}
|
||||||
|
defaultValue={1}
|
||||||
|
onChange={(value) => this.setState({columnNum: value})}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
columnNum: 1,
|
||||||
|
rowNum: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormDialog;
|
146
src/component/Dialog/HistoryDialog.js
Normal file
146
src/component/Dialog/HistoryDialog.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {Modal, Empty, message} from "antd";
|
||||||
|
import LocalHistory from "../LocalHistory";
|
||||||
|
import {AutoSaveInterval, getLocalDocuments, setLocalDocuments, setLocalDraft} from "../LocalHistory/util";
|
||||||
|
import IndexDB from "../LocalHistory/indexdb";
|
||||||
|
import debouce from "lodash.debounce";
|
||||||
|
|
||||||
|
const DocumentID = 1;
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@inject("content")
|
||||||
|
@observer
|
||||||
|
class HistoryDialog extends Component {
|
||||||
|
timer = null;
|
||||||
|
|
||||||
|
db = null;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
documents: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
await this.initIndexDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
get editor() {
|
||||||
|
return this.props.content.markdownEditor;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// async UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
|
// // 文档 id 变更
|
||||||
|
// if (this.props.documentID !== nextProps.documentID && nextProps.documentID != null) {
|
||||||
|
// if (this.db) {
|
||||||
|
// await this.overrideLocalDocuments(nextProps.documentID);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
|
||||||
|
closeDialog = () => {
|
||||||
|
this.props.dialog.setHistoryOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
editLocalDocument = (content) => {
|
||||||
|
this.props.content.setContent(content);
|
||||||
|
message.success("恢复成功!");
|
||||||
|
this.closeDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
autoSave = async (isRecent = false) => {
|
||||||
|
const Content = this.props.content.markdownEditor.getValue();
|
||||||
|
if (Content.trim() !== "") {
|
||||||
|
const document = {
|
||||||
|
Content,
|
||||||
|
DocumentID: this.props.documentID,
|
||||||
|
SaveTime: new Date(),
|
||||||
|
};
|
||||||
|
const setLocalDocumentMethod = isRecent && this.state.documents.length > 0 ? setLocalDraft : setLocalDocuments;
|
||||||
|
await setLocalDocumentMethod(this.db, this.state.documents, document);
|
||||||
|
await this.overrideLocalDocuments(this.props.documentID);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async initIndexDB() {
|
||||||
|
try {
|
||||||
|
const indexDB = new IndexDB({
|
||||||
|
name: "mdnice-local-history",
|
||||||
|
storeName: "customers",
|
||||||
|
storeOptions: {keyPath: "id", autoIncrement: true},
|
||||||
|
storeInit: (objectStore) => {
|
||||||
|
objectStore.createIndex("DocumentID", "DocumentID", {unique: false});
|
||||||
|
objectStore.createIndex("SaveTime", "SaveTime", {unique: false});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.db = await indexDB.init();
|
||||||
|
|
||||||
|
if (this.db && this.props.documentID) {
|
||||||
|
await this.overrideLocalDocuments(this.props.documentID);
|
||||||
|
}
|
||||||
|
// 每隔一段时间自动保存
|
||||||
|
this.timer = setInterval(async () => {
|
||||||
|
await this.autoSave();
|
||||||
|
}, AutoSaveInterval);
|
||||||
|
// 每改变内容自动保存最近的一条
|
||||||
|
this.editor.on &&
|
||||||
|
this.editor.on(
|
||||||
|
"change",
|
||||||
|
debouce(async () => {
|
||||||
|
await this.autoSave(true);
|
||||||
|
}, 1000),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新本地历史文档
|
||||||
|
async overrideLocalDocuments(documentID) {
|
||||||
|
const localDocuments = await getLocalDocuments(this.db, +documentID);
|
||||||
|
// console.log('refresh local',localDocuments);
|
||||||
|
this.setState({
|
||||||
|
documents: localDocuments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="nice-md-local-history"
|
||||||
|
title="本地历史"
|
||||||
|
centered
|
||||||
|
width={1080}
|
||||||
|
visible={this.props.dialog.isHistoryOpen}
|
||||||
|
onCancel={this.closeDialog}
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
{this.state.documents && this.state.documents.length > 0 ? (
|
||||||
|
<LocalHistory
|
||||||
|
content={this.props.content.content}
|
||||||
|
documents={this.state.documents}
|
||||||
|
documentID={this.props.documentID}
|
||||||
|
onEdit={this.editLocalDocument}
|
||||||
|
onCancel={this.closeDialog}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Empty style={{width: "100%"}} description="暂无本地历史" />
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryDialog.defaultProps = {
|
||||||
|
documentID: DocumentID,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryDialog;
|
180
src/component/Dialog/ImageDialog.js
Normal file
180
src/component/Dialog/ImageDialog.js
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {Modal, Upload, Tabs, Select} from "antd";
|
||||||
|
|
||||||
|
import SvgIcon from "../../icon";
|
||||||
|
|
||||||
|
import AliOSS from "../ImageHosting/AliOSS";
|
||||||
|
import QiniuOSS from "../ImageHosting/QiniuOSS";
|
||||||
|
import Gitee from "../ImageHosting/Gitee";
|
||||||
|
import GitHub from "../ImageHosting/GitHub";
|
||||||
|
|
||||||
|
import {uploadAdaptor} from "../../utils/imageHosting";
|
||||||
|
import {SM_MS_PROXY, IMAGE_HOSTING_TYPE, IMAGE_HOSTING_NAMES} from "../../utils/constant";
|
||||||
|
import appContext from "../../utils/appContext";
|
||||||
|
|
||||||
|
const {Dragger} = Upload;
|
||||||
|
const {TabPane} = Tabs;
|
||||||
|
const {Option} = Select;
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@inject("content")
|
||||||
|
@inject("imageHosting")
|
||||||
|
@inject("navbar")
|
||||||
|
@observer
|
||||||
|
class ImageDialog extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.images = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认后将内容更新到编辑器上
|
||||||
|
handleOk = () => {
|
||||||
|
let text = "";
|
||||||
|
// 成功后添加url
|
||||||
|
if (this.props.navbar.isContainImgName) {
|
||||||
|
this.images.forEach((value) => {
|
||||||
|
text += `\n`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.images.forEach((value) => {
|
||||||
|
text += `\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 重新初始化
|
||||||
|
this.images = [];
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const cursor = markdownEditor.getCursor();
|
||||||
|
markdownEditor.replaceSelection(text, cursor);
|
||||||
|
// 上传后实时更新内容
|
||||||
|
const content = markdownEditor.getValue();
|
||||||
|
this.props.content.setContent(content);
|
||||||
|
|
||||||
|
this.props.dialog.setImageOpen(false);
|
||||||
|
cursor.ch += 2;
|
||||||
|
markdownEditor.setCursor(cursor);
|
||||||
|
markdownEditor.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCancel = () => {
|
||||||
|
this.props.dialog.setImageOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
customRequest = ({action, data, file, headers, onError, onProgress, onSuccess, withCredentials}) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
const {images} = this;
|
||||||
|
if (data) {
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
formData.append(key, data[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 使用阿里云图床
|
||||||
|
if (this.props.imageHosting.type === "阿里云") {
|
||||||
|
uploadAdaptor({file, onSuccess, onError, images});
|
||||||
|
}
|
||||||
|
// 使用七牛云图床
|
||||||
|
else if (this.props.imageHosting.type === "七牛云") {
|
||||||
|
uploadAdaptor({file, onSuccess, onError, onProgress, images});
|
||||||
|
}
|
||||||
|
// 使用SM.MS图床
|
||||||
|
else if (this.props.imageHosting.type === "SM.MS") {
|
||||||
|
uploadAdaptor({formData, file, action, onProgress, onSuccess, onError, headers, withCredentials});
|
||||||
|
}
|
||||||
|
// 使用Gitee图床
|
||||||
|
else if (this.props.imageHosting.type === "Gitee") {
|
||||||
|
uploadAdaptor({formData, file, action, onProgress, onSuccess, onError, headers, withCredentials, images});
|
||||||
|
}
|
||||||
|
// 使用GitHub图床
|
||||||
|
else if (this.props.imageHosting.type === "GitHub") {
|
||||||
|
uploadAdaptor({formData, file, action, onProgress, onSuccess, onError, headers, withCredentials, images});
|
||||||
|
}
|
||||||
|
// 使用用户提供的图床或是默认mdnice图床
|
||||||
|
else {
|
||||||
|
uploadAdaptor({formData, file, onSuccess, onError, images});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
abort() {
|
||||||
|
console.log("upload progress is aborted.");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
typeChange = (type) => {
|
||||||
|
this.props.imageHosting.setType(type);
|
||||||
|
window.localStorage.setItem(IMAGE_HOSTING_TYPE, type);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {hostingList, type} = this.props.imageHosting;
|
||||||
|
|
||||||
|
const columns = hostingList.map((option, index) => (
|
||||||
|
<Option key={index} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</Option>
|
||||||
|
));
|
||||||
|
|
||||||
|
const imageHostingSwitch = (
|
||||||
|
<Select style={{width: "90px"}} value={type} onChange={this.typeChange}>
|
||||||
|
{columns}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="本地上传"
|
||||||
|
okText="确认"
|
||||||
|
cancelText="取消"
|
||||||
|
visible={this.props.dialog.isImageOpen}
|
||||||
|
onOk={this.handleOk}
|
||||||
|
onCancel={this.handleCancel}
|
||||||
|
bodyStyle={{paddingTop: "10px"}}
|
||||||
|
>
|
||||||
|
<appContext.Consumer>
|
||||||
|
{({useImageHosting}) => (
|
||||||
|
<Tabs tabBarExtraContent={imageHostingSwitch} type="card">
|
||||||
|
<TabPane tab="图片上传" key="1">
|
||||||
|
<Dragger name="file" multiple action={SM_MS_PROXY} customRequest={this.customRequest}>
|
||||||
|
<p className="ant-upload-drag-icon">
|
||||||
|
<SvgIcon name="inbox" style={style.svgIcon} fill="#40a9ff" />
|
||||||
|
</p>
|
||||||
|
<p className="ant-upload-text">点击或拖拽一张或多张照片上传</p>
|
||||||
|
<p className="ant-upload-hint">{"正在使用" + type + "图床"}</p>
|
||||||
|
</Dragger>
|
||||||
|
</TabPane>
|
||||||
|
{useImageHosting.isAliyunOpen ? (
|
||||||
|
<TabPane tab={IMAGE_HOSTING_NAMES.aliyun} key="2">
|
||||||
|
<AliOSS />
|
||||||
|
</TabPane>
|
||||||
|
) : null}
|
||||||
|
{useImageHosting.isQiniuyunOpen ? (
|
||||||
|
<TabPane tab={IMAGE_HOSTING_NAMES.qiniuyun} key="3">
|
||||||
|
<QiniuOSS />
|
||||||
|
</TabPane>
|
||||||
|
) : null}
|
||||||
|
{useImageHosting.isGiteeOpen ? (
|
||||||
|
<TabPane tab={IMAGE_HOSTING_NAMES.gitee} key="4">
|
||||||
|
<Gitee />
|
||||||
|
</TabPane>
|
||||||
|
) : null}
|
||||||
|
{useImageHosting.isGitHubOpen ? (
|
||||||
|
<TabPane tab={IMAGE_HOSTING_NAMES.github} key="5">
|
||||||
|
<GitHub />
|
||||||
|
</TabPane>
|
||||||
|
) : null}
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
|
</appContext.Consumer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
svgIcon: {
|
||||||
|
width: "48px",
|
||||||
|
height: "48px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImageDialog;
|
61
src/component/Dialog/LinkDialog.js
Normal file
61
src/component/Dialog/LinkDialog.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {Modal, Input, Form} from "antd";
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@inject("content")
|
||||||
|
@observer
|
||||||
|
class LinkDialog extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
link: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOk = () => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const cursor = markdownEditor.getCursor();
|
||||||
|
const selection = markdownEditor.getSelection();
|
||||||
|
const text = `[${selection}](${this.state.link})`;
|
||||||
|
markdownEditor.replaceSelection(text, cursor);
|
||||||
|
|
||||||
|
// 上传后实时更新内容
|
||||||
|
const content = markdownEditor.getValue();
|
||||||
|
this.props.content.setContent(content);
|
||||||
|
|
||||||
|
this.setState({link: ""});
|
||||||
|
this.props.dialog.setLinkOpen(false);
|
||||||
|
cursor.ch += 1;
|
||||||
|
markdownEditor.setCursor(cursor);
|
||||||
|
markdownEditor.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCancel = () => {
|
||||||
|
this.setState({link: ""});
|
||||||
|
this.props.dialog.setLinkOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleChange = (e) => {
|
||||||
|
this.setState({link: e.target.value});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="添加链接"
|
||||||
|
okText="确认"
|
||||||
|
cancelText="取消"
|
||||||
|
visible={this.props.dialog.isLinkOpen}
|
||||||
|
onOk={this.handleOk}
|
||||||
|
onCancel={this.handleCancel}
|
||||||
|
>
|
||||||
|
<Form.Item label="链接地址">
|
||||||
|
<Input placeholder="请输入链接地址" value={this.state.link} onChange={this.handleChange} />
|
||||||
|
</Form.Item>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LinkDialog;
|
106
src/component/Dialog/SitDownDialog.js
Normal file
106
src/component/Dialog/SitDownDialog.js
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {Modal, Input, Select, message} from "antd";
|
||||||
|
|
||||||
|
import SitDownConverter from "../../utils/sitdownConverter";
|
||||||
|
import {SITDOWN_OPTIONS} from "../../utils/constant";
|
||||||
|
|
||||||
|
const {Option} = Select;
|
||||||
|
const {TextArea} = Input;
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@inject("content")
|
||||||
|
@observer
|
||||||
|
class SitDownDialog extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
platform: "default",
|
||||||
|
sourceCode: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOk = () => {
|
||||||
|
try {
|
||||||
|
const {platform, sourceCode} = this.state;
|
||||||
|
|
||||||
|
const domParser = new DOMParser();
|
||||||
|
const sourceCodeDom = domParser.parseFromString(sourceCode, "text/html");
|
||||||
|
|
||||||
|
let content = "";
|
||||||
|
|
||||||
|
if (platform === "csdn") {
|
||||||
|
const articleDom = sourceCodeDom.getElementById("content_views");
|
||||||
|
content = SitDownConverter.CSDN(articleDom);
|
||||||
|
} else if (platform === "juejin") {
|
||||||
|
const articleDom = sourceCodeDom.getElementsByClassName("article-content");
|
||||||
|
content = SitDownConverter.Juejin(articleDom[0]);
|
||||||
|
} else if (platform === "zhihu") {
|
||||||
|
const articleDom = sourceCodeDom.getElementsByClassName("Post-RichText");
|
||||||
|
content = SitDownConverter.Zhihu(articleDom[0]);
|
||||||
|
} else if (platform === "wechat") {
|
||||||
|
const articleDom = sourceCodeDom.getElementById("js_content");
|
||||||
|
content = SitDownConverter.Wechat(articleDom);
|
||||||
|
} else {
|
||||||
|
content = SitDownConverter.GFM(sourceCodeDom);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.content.setContent(content);
|
||||||
|
|
||||||
|
this.props.dialog.setSitDownOpen(false);
|
||||||
|
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
// const cursor = markdownEditor.getCursor();
|
||||||
|
// cursor.ch += 1;
|
||||||
|
// markdownEditor.setCursor(cursor);
|
||||||
|
markdownEditor.focus();
|
||||||
|
} catch (e) {
|
||||||
|
message.error("源代码与已选平台的文章域名不符");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCancel = () => {
|
||||||
|
this.props.dialog.setSitDownOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
handlePlatform = (value) => {
|
||||||
|
this.setState({platform: value});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSourceCode = (e) => {
|
||||||
|
this.setState({sourceCode: e.target.value});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {sourceCode, platform} = this.state;
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="html 转 markdown"
|
||||||
|
okText="转换"
|
||||||
|
cancelText="取消"
|
||||||
|
visible={this.props.dialog.isSitDownOpen}
|
||||||
|
onOk={this.handleOk}
|
||||||
|
onCancel={this.handleCancel}
|
||||||
|
>
|
||||||
|
<Select value={platform} style={{width: 300, marginBottom: "20px"}} onChange={this.handlePlatform}>
|
||||||
|
{SITDOWN_OPTIONS.map((option) => (
|
||||||
|
<Option key={option.key} value={option.key}>
|
||||||
|
{option.value}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
rows={4}
|
||||||
|
style={{marginBottom: "5px"}}
|
||||||
|
value={sourceCode}
|
||||||
|
onChange={this.handleSourceCode}
|
||||||
|
placeholder="请放入网页源代码"
|
||||||
|
/>
|
||||||
|
<span>提示:右键->显示网页源代码->全选->复制粘贴。</span>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SitDownDialog;
|
3
src/component/Dialog/VersionDialog.css
Normal file
3
src/component/Dialog/VersionDialog.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.specialInfo > * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
155
src/component/Dialog/VersionDialog.js
Normal file
155
src/component/Dialog/VersionDialog.js
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {Modal, Timeline, Button} from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
import {NEWEST_VERSION} from "../../utils/constant";
|
||||||
|
import SvgIcon from "../../icon";
|
||||||
|
|
||||||
|
import "./VersionDialog.css";
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class VersionDialog extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
// eslint-disable-next-line react/no-unused-state
|
||||||
|
versionNumber: 0,
|
||||||
|
versionTimeline: [],
|
||||||
|
recommend: null,
|
||||||
|
specialInfo: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOk = () => {
|
||||||
|
this.props.dialog.setVersionOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCancel = () => {
|
||||||
|
this.props.dialog.setVersionOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMore = () => {
|
||||||
|
const w = window.open("about:blank");
|
||||||
|
w.location.href = "https://git.luckday.cn/Markdown2Html/master/CHANGELOG.md";
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDocs = () => {
|
||||||
|
const w = window.open("about:blank");
|
||||||
|
w.location.href = "https://github.com/shenweiyan/Markdown2Html";
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount = async () => {
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
versionId: 1,
|
||||||
|
versionNumber: "1.0.0",
|
||||||
|
versionTimeline: ["2023-09-20 增加网格黑主题", "2023-09-14 解决超链接文字复制到公众号颜色失效的问题", "2023-09-01 优化部分配置与信息", "2023-08-30 Fork 自 markdown2html"],
|
||||||
|
recommend: {
|
||||||
|
link: "https://github.com/shenweiyan/Knowledge-Garden",
|
||||||
|
mainInfo: "欢迎关注我的知识花园",
|
||||||
|
},
|
||||||
|
specialInfo: ''
|
||||||
|
//specialInfo:
|
||||||
|
// '<div style="display:flex;justify-content:center;align-items:center;"><img style="width:50%;" src="http://md.aizhuanqian.online/img/wechat_qr.df324554.jpeg"/></div>',
|
||||||
|
};
|
||||||
|
const newestVersion = localStorage.getItem(NEWEST_VERSION);
|
||||||
|
if (data.versionNumber !== newestVersion) {
|
||||||
|
this.props.dialog.setVersionOpen(true);
|
||||||
|
localStorage.setItem(NEWEST_VERSION, data.versionNumber);
|
||||||
|
}
|
||||||
|
this.setState({...data});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("读取最新版本信息错误");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="版本更新与说明"
|
||||||
|
visible={this.props.dialog.isVersionOpen}
|
||||||
|
onOk={this.handleOk}
|
||||||
|
onCancel={this.handleCancel}
|
||||||
|
footer={[
|
||||||
|
<Button key="submit" type="primary" onClick={this.handleOk}>
|
||||||
|
确认
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Timeline>
|
||||||
|
<Timeline.Item dot={<SvgIcon name="environment" style={style.svgIcon} />}>
|
||||||
|
<strong>更多版本更新与说明信息请查看
|
||||||
|
<a
|
||||||
|
id="more-info"
|
||||||
|
style={{fontWeight: "bold", borderBottom: "solid"}}
|
||||||
|
alt=""
|
||||||
|
href="https://github.com/shenweiyan/Markdown2Html"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
这里
|
||||||
|
</a>
|
||||||
|
</strong>
|
||||||
|
</Timeline.Item>
|
||||||
|
{this.state.versionTimeline.map((version, index) => {
|
||||||
|
/*if (index === 0) {
|
||||||
|
return (
|
||||||
|
<Timeline.Item key={index} dot={<SvgIcon name="environment" style={style.svgIcon} />}>
|
||||||
|
<strong>{version}</strong>
|
||||||
|
</Timeline.Item>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <Timeline.Item key={index}>{version}</Timeline.Item>;
|
||||||
|
}*/
|
||||||
|
return <Timeline.Item key={index}>{version}</Timeline.Item>;
|
||||||
|
})}
|
||||||
|
<Timeline.Item>
|
||||||
|
了解更多,请查看
|
||||||
|
<a
|
||||||
|
id="nice-version-dialog-doc"
|
||||||
|
style={{fontWeight: "bold"}}
|
||||||
|
alt=""
|
||||||
|
href="https://github.com/shenweiyan/Markdown2Html"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
本仓库源码与说明
|
||||||
|
</a>
|
||||||
|
</Timeline.Item>
|
||||||
|
{this.state.recommend && (
|
||||||
|
<Timeline.Item dot={<SvgIcon name="more" style={style.svgIcon} />}>
|
||||||
|
<a
|
||||||
|
id="nice-version-dialog-recommend"
|
||||||
|
style={{fontWeight: "bold", borderBottom: "double"}}
|
||||||
|
alt=""
|
||||||
|
href={this.state.recommend.link}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{this.state.recommend.mainInfo}
|
||||||
|
</a>
|
||||||
|
</Timeline.Item>
|
||||||
|
)}
|
||||||
|
</Timeline>
|
||||||
|
{this.state.specialInfo && (
|
||||||
|
<div
|
||||||
|
id="nice-version-dialog-special"
|
||||||
|
dangerouslySetInnerHTML={{__html: this.state.specialInfo}}
|
||||||
|
className="specialInfo"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
svgIcon: {
|
||||||
|
width: "16px",
|
||||||
|
height: "16px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VersionDialog;
|
96
src/component/ImageHosting/AliOSS.js
Normal file
96
src/component/ImageHosting/AliOSS.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {Input, Form} from "antd";
|
||||||
|
import {ALIOSS_IMAGE_HOSTING} from "../../utils/constant";
|
||||||
|
|
||||||
|
const formItemLayout = {
|
||||||
|
labelCol: {
|
||||||
|
xs: {span: 6},
|
||||||
|
},
|
||||||
|
wrapperCol: {
|
||||||
|
xs: {span: 16},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
@inject("imageHosting")
|
||||||
|
@observer
|
||||||
|
class AliOSS extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
// 从localstorage里面读取
|
||||||
|
const imageHosting = JSON.parse(localStorage.getItem(ALIOSS_IMAGE_HOSTING));
|
||||||
|
this.state = {
|
||||||
|
imageHosting,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
regionChange = (e) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.region = e.target.value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(ALIOSS_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
accessKeyIdChange = (e) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.accessKeyId = e.target.value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(ALIOSS_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
accessKeySecretChange = (e) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.accessKeySecret = e.target.value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(ALIOSS_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
bucketChange = (e) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.bucket = e.target.value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(ALIOSS_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {region, accessKeyId, accessKeySecret, bucket} = this.state.imageHosting;
|
||||||
|
return (
|
||||||
|
<Form {...formItemLayout}>
|
||||||
|
<Form.Item label="Bucket" style={style.formItem}>
|
||||||
|
<Input value={bucket} onChange={this.bucketChange} placeholder="例如:my-wechat" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="Region" style={style.formItem}>
|
||||||
|
<Input value={region} onChange={this.regionChange} placeholder="例如:oss-cn-hangzhou" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="AccessKey ID" style={style.formItem}>
|
||||||
|
<Input value={accessKeyId} onChange={this.accessKeyIdChange} placeholder="例如:qweASDF1234zxcvb" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="AccessKey Secret" style={style.formItem}>
|
||||||
|
<Input
|
||||||
|
value={accessKeySecret}
|
||||||
|
onChange={this.accessKeySecretChange}
|
||||||
|
placeholder="例如:qweASDF1234zxcvbqweASD"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="提示" style={style.formItem}>
|
||||||
|
<span>配置后请在右上角进行切换,</span>
|
||||||
|
<a
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href="https://preview.mdnice.com/article/developer/aliyun-image-hosting/"
|
||||||
|
>
|
||||||
|
阿里云图床配置文档
|
||||||
|
</a>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
formItem: {
|
||||||
|
marginBottom: "10px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AliOSS;
|
95
src/component/ImageHosting/GitHub.js
Normal file
95
src/component/ImageHosting/GitHub.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {Input, Form, Checkbox} from "antd";
|
||||||
|
import {GITHUB_IMAGE_HOSTING} from "../../utils/constant";
|
||||||
|
|
||||||
|
const formItemLayout = {
|
||||||
|
labelCol: {
|
||||||
|
xs: {span: 6},
|
||||||
|
},
|
||||||
|
wrapperCol: {
|
||||||
|
xs: {span: 16},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
@inject("imageHosting")
|
||||||
|
@observer
|
||||||
|
class Gitee extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
// 从localstorage里面读取
|
||||||
|
const imageHosting = JSON.parse(localStorage.getItem(GITHUB_IMAGE_HOSTING));
|
||||||
|
this.state = {
|
||||||
|
imageHosting,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
usernameChange = (e) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.username = e.target.value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(GITHUB_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
repoChange = (e) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.repo = e.target.value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(GITHUB_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
tokenChange = (e) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.token = e.target.value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(GITHUB_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
jsdelivrChange = (e) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.jsdelivr = e.target.checked ? "true" : "false";
|
||||||
|
console.log(imageHosting);
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(GITHUB_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {username, repo, token, jsdelivr} = this.state.imageHosting;
|
||||||
|
return (
|
||||||
|
<Form {...formItemLayout}>
|
||||||
|
<Form.Item label="用户名" style={style.formItem}>
|
||||||
|
<Input value={username} onChange={this.usernameChange} placeholder="例如:mdnice" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="仓库名" style={style.formItem}>
|
||||||
|
<Input value={repo} onChange={this.repoChange} placeholder="例如:picture" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="token" style={style.formItem}>
|
||||||
|
<Input value={token} onChange={this.tokenChange} placeholder="例如:qweASDF1234zxcvb" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="jsDelivr CDN" style={style.formItem}>
|
||||||
|
<Checkbox checked={jsdelivr === "true"} onChange={this.jsdelivrChange}>
|
||||||
|
(强烈建议开启,加速图片)
|
||||||
|
</Checkbox>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="提示" style={style.formItem}>
|
||||||
|
<span>配置后请在右上角进行切换,</span>
|
||||||
|
<a
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href="https://preview.mdnice.com/article/developer/github-image-hosting/"
|
||||||
|
>
|
||||||
|
GitHub图床配置文档
|
||||||
|
</a>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
formItem: {
|
||||||
|
marginBottom: "10px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Gitee;
|
82
src/component/ImageHosting/Gitee.js
Normal file
82
src/component/ImageHosting/Gitee.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {Input, Form} from "antd";
|
||||||
|
import {GITEE_IMAGE_HOSTING} from "../../utils/constant";
|
||||||
|
|
||||||
|
const formItemLayout = {
|
||||||
|
labelCol: {
|
||||||
|
xs: {span: 6},
|
||||||
|
},
|
||||||
|
wrapperCol: {
|
||||||
|
xs: {span: 16},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
@inject("imageHosting")
|
||||||
|
@observer
|
||||||
|
class Gitee extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
// 从localstorage里面读取
|
||||||
|
const imageHosting = JSON.parse(localStorage.getItem(GITEE_IMAGE_HOSTING));
|
||||||
|
this.state = {
|
||||||
|
imageHosting,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
usernameChange = (e) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.username = e.target.value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(GITEE_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
repoChange = (e) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.repo = e.target.value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(GITEE_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
tokenChange = (e) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.token = e.target.value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(GITEE_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {username, repo, token} = this.state.imageHosting;
|
||||||
|
return (
|
||||||
|
<Form {...formItemLayout}>
|
||||||
|
<Form.Item label="用户名" style={style.formItem}>
|
||||||
|
<Input value={username} onChange={this.usernameChange} placeholder="例如:mdnice" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="仓库名" style={style.formItem}>
|
||||||
|
<Input value={repo} onChange={this.repoChange} placeholder="例如:picture" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="token" style={style.formItem}>
|
||||||
|
<Input value={token} onChange={this.tokenChange} placeholder="例如:qweASDF1234zxcvb" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="提示" style={style.formItem}>
|
||||||
|
<span>配置后请在右上角进行切换,</span>
|
||||||
|
<a
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href="https://preview.mdnice.com/article/developer/gitee-image-hosting/"
|
||||||
|
>
|
||||||
|
Gitee 图床配置文档
|
||||||
|
</a>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
formItem: {
|
||||||
|
marginBottom: "10px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Gitee;
|
124
src/component/ImageHosting/QiniuOSS.js
Normal file
124
src/component/ImageHosting/QiniuOSS.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {Input, Select, Form} from "antd";
|
||||||
|
import {QINIUOSS_IMAGE_HOSTING} from "../../utils/constant";
|
||||||
|
|
||||||
|
const {Option} = Select;
|
||||||
|
const formItemLayout = {
|
||||||
|
labelCol: {
|
||||||
|
xs: {span: 6},
|
||||||
|
},
|
||||||
|
wrapperCol: {
|
||||||
|
xs: {span: 16},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
@inject("imageHosting")
|
||||||
|
@observer
|
||||||
|
class QiniuOSS extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
// 从localstorage里面读取
|
||||||
|
const imageHosting = JSON.parse(localStorage.getItem(QINIUOSS_IMAGE_HOSTING));
|
||||||
|
const link = imageHosting.domain.split("://")[1];
|
||||||
|
this.state = {
|
||||||
|
imageHosting,
|
||||||
|
link,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
regionChange = (value) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.region = value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(QINIUOSS_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
accessKeyChange = (e) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.accessKey = e.target.value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(QINIUOSS_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
secretKeyChange = (e) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.secretKey = e.target.value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(QINIUOSS_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
bucketChange = (e) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.bucket = e.target.value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(QINIUOSS_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
linkChange = (e) => {
|
||||||
|
this.setState({link: e.target.value});
|
||||||
|
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.domain = "https://" + e.target.value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(QINIUOSS_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
namespaceChange = ({target: {value}}) => {
|
||||||
|
const {imageHosting} = this.state;
|
||||||
|
imageHosting.namespace = value;
|
||||||
|
this.setState({imageHosting});
|
||||||
|
localStorage.setItem(QINIUOSS_IMAGE_HOSTING, JSON.stringify(imageHosting));
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {region, accessKey, secretKey, bucket, namespace} = this.state.imageHosting;
|
||||||
|
const {link} = this.state;
|
||||||
|
return (
|
||||||
|
<Form {...formItemLayout}>
|
||||||
|
<Form.Item label="存储空间名称" style={style.formItem}>
|
||||||
|
<Input value={bucket} onChange={this.bucketChange} placeholder="例如:my-wechat" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="存储区域" style={style.formItem}>
|
||||||
|
<Select value={region} onChange={this.regionChange} placeholder="例如:qiniu.region.z2">
|
||||||
|
<Option value="z0">华东</Option>
|
||||||
|
<Option value="z1">华北</Option>
|
||||||
|
<Option value="z2">华南</Option>
|
||||||
|
<Option value="na0">北美</Option>
|
||||||
|
<Option value="as0">东南亚</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="AccessKey" style={style.formItem}>
|
||||||
|
<Input value={accessKey} onChange={this.accessKeyChange} placeholder="例如:qweASDF1234zxcvb" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="SecretKey" style={style.formItem}>
|
||||||
|
<Input value={secretKey} onChange={this.secretKeyChange} placeholder="例如:qweASDF1234zxcvbqweASD" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="自定义域名" style={style.formItem}>
|
||||||
|
<Input value={link} onChange={this.linkChange} addonBefore="https://" placeholder="例如:qiniu.mdnice.com" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="自定义命名空间" style={style.formItem}>
|
||||||
|
<Input value={namespace} onChange={this.namespaceChange} placeholder="例如:image/" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="提示" style={style.formItem}>
|
||||||
|
<span>配置后请在右上角进行切换,</span>
|
||||||
|
<a
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href="https://preview.mdnice.com/article/developer/qiniu-image-hosting/"
|
||||||
|
>
|
||||||
|
七牛云图床配置文档
|
||||||
|
</a>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
formItem: {
|
||||||
|
marginBottom: "10px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QiniuOSS;
|
113
src/component/LocalHistory/index.js
Normal file
113
src/component/LocalHistory/index.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import {Menu, Button, Radio} from "antd";
|
||||||
|
import CodeMirror from "@uiw/react-codemirror";
|
||||||
|
import {diff_match_patch as DiffMatchPath} from "diff-match-patch";
|
||||||
|
|
||||||
|
import "./localHistory.css";
|
||||||
|
|
||||||
|
const NOOP = () => {};
|
||||||
|
const prefix = "nice-md-local-history";
|
||||||
|
|
||||||
|
const diff = new DiffMatchPath();
|
||||||
|
|
||||||
|
class LocalHistory extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const {documents} = this.props;
|
||||||
|
this.state = {
|
||||||
|
content: documents[0].Content,
|
||||||
|
selectedKeys: String(documents[0].id),
|
||||||
|
mode: "all",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getDiffHtml = () => {
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
var a = diff.diff_linesToChars_(this.state.content, this.props.content);
|
||||||
|
var lineText1 = a.chars1;
|
||||||
|
var lineText2 = a.chars2;
|
||||||
|
var diffs = diff.diff_main(lineText1, lineText2, false);
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
diff.diff_charsToLines_(diffs, a.lineArray);
|
||||||
|
const html = diff
|
||||||
|
.diff_prettyHtml(diffs)
|
||||||
|
.replace(/¶/g, "")
|
||||||
|
.replace(/<br>/g, "​<br>​");
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
|
||||||
|
selectNav = ({selectedKeys}) => {
|
||||||
|
const {Content: content} = this.props.documents.find((doc) => String(doc.id) === String(selectedKeys[0])) || {};
|
||||||
|
this.setState({
|
||||||
|
content,
|
||||||
|
selectedKeys,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleModeChange = (e) => {
|
||||||
|
this.setState({
|
||||||
|
mode: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {documents} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Menu className={`${prefix}-nav`} onSelect={this.selectNav} selectedKeys={this.state.selectedKeys}>
|
||||||
|
{documents.map((d) => (
|
||||||
|
<Menu.Item key={d.id}>{d.SaveTime.toLocaleString()}</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
{this.state.content && (
|
||||||
|
<div className={`${prefix}-preview`}>
|
||||||
|
{this.state.mode === "all" ? (
|
||||||
|
<CodeMirror
|
||||||
|
key="local-history"
|
||||||
|
value={this.state.content}
|
||||||
|
height="calc(100% - 56px)"
|
||||||
|
options={{
|
||||||
|
readOnly: true,
|
||||||
|
theme: "md-mirror",
|
||||||
|
mode: "markdown",
|
||||||
|
lineWrapping: true,
|
||||||
|
lineNumbers: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div dangerouslySetInnerHTML={{__html: this.getDiffHtml()}} className={`${prefix}-diff-content`} />
|
||||||
|
)}
|
||||||
|
<div className={`${prefix}-btn-group`}>
|
||||||
|
<Radio.Group onChange={this.handleModeChange} value={this.state.mode}>
|
||||||
|
<Radio value="all">全文</Radio>
|
||||||
|
<Radio value="diff">和当前内容对比</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
<div>
|
||||||
|
<Button onClick={this.props.onCancel}>取消</Button>
|
||||||
|
<Button
|
||||||
|
id="nice-local-history-review"
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
this.props.onEdit(this.state.content);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
恢复此版本
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalHistory.defaultProps = {
|
||||||
|
visible: false,
|
||||||
|
document: [{}],
|
||||||
|
onEdit: NOOP,
|
||||||
|
onCancel: NOOP,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LocalHistory;
|
63
src/component/LocalHistory/indexdb.js
Normal file
63
src/component/LocalHistory/indexdb.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import {message} from "antd";
|
||||||
|
|
||||||
|
// In the following line, you should include the prefixes of implementations you want to test.
|
||||||
|
const indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
|
||||||
|
// DON'T use "var indexedDB = ..." if you're not in a function.
|
||||||
|
// Moreover, you may need references to some window.IDB* objects:
|
||||||
|
// const IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction;
|
||||||
|
// const IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
|
||||||
|
|
||||||
|
// (Mozilla has never prefixed these objects, so we don't need window.mozIDB*)
|
||||||
|
|
||||||
|
export default class IndexDB {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (!indexedDB) {
|
||||||
|
message.error("初始化 indexdb 失败!浏览器不支持");
|
||||||
|
throw Error("浏览器不支持 indexdb");
|
||||||
|
}
|
||||||
|
|
||||||
|
const {name, storeName = "", storeOptions = {}, storeInit = () => {}} = this.options;
|
||||||
|
|
||||||
|
this.storeName = storeName;
|
||||||
|
this.storeOptions = storeOptions;
|
||||||
|
this.storeInit = storeInit;
|
||||||
|
|
||||||
|
const request = indexedDB.open(name);
|
||||||
|
const result = await this.initEvent(request);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
initEvent(request) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request.onerror = (event) => {
|
||||||
|
// Do something with request.errorCode!
|
||||||
|
message.error("初始化数据库失败!", event.target.errorCode);
|
||||||
|
reject(new Error("初始化数据库失败!"));
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const db = event.target.result;
|
||||||
|
console.log("成功初始化数据库");
|
||||||
|
// this.db = db;
|
||||||
|
resolve(db);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 该事件仅在较新的浏览器中被实现
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
// 更新对象存储空间和索引 ....
|
||||||
|
const db = event.target.result;
|
||||||
|
this.initStore(db, this.storeName, this.storeOptions, this.storeInit);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initStore(db, name, options, func) {
|
||||||
|
// 创建一个对象存储空间来持有信息。
|
||||||
|
const objectStore = db.createObjectStore(name, options);
|
||||||
|
if (func) func(objectStore);
|
||||||
|
}
|
||||||
|
}
|
66
src/component/LocalHistory/localHistory.css
Normal file
66
src/component/LocalHistory/localHistory.css
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
.nice-md-local-history .ant-modal-body {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - 112px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-md-local-history .CodeMirror-merge,
|
||||||
|
.nice-md-local-history .CodeMirror-merge .CodeMirror {
|
||||||
|
height: calc(100vh - 216px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-md-local-history .CodeMirror-merge-copy {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-md-local-history-preview {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 776px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-md-local-history-btn-group {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
.nice-md-local-history-btn-group button {
|
||||||
|
margin: 25px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-md-local-history-btn-group button:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-md-local-history-nav {
|
||||||
|
width: 256px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-md-local-history-nav .ant-list-item {
|
||||||
|
border-bottom: 0;
|
||||||
|
padding: 17px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-md-local-history-diff-content {
|
||||||
|
width: inherit;
|
||||||
|
background-color: rgba(245, 245, 245, 1);
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding: 24px;
|
||||||
|
height: calc(100% - 56px);
|
||||||
|
color: #202020;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: 'source-code-pro', Menlo, 'Courier New', Consolas, monospace, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||||
|
font-variant-caps: normal;
|
||||||
|
font-variant-east-asian: normal;
|
||||||
|
font-variant-ligatures: contextual;
|
||||||
|
line-height: 25px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-feature-settings: "calt";
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
106
src/component/LocalHistory/util.js
Normal file
106
src/component/LocalHistory/util.js
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import {message} from "antd";
|
||||||
|
|
||||||
|
function saveTimeSort(a, b) {
|
||||||
|
return new Date(b.SaveTime).getTime() - new Date(a.SaveTime).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MaxLocalDocumentLength = 30;
|
||||||
|
|
||||||
|
// export const AutoSaveInterval = 10 * 60 * 1000;
|
||||||
|
export const AutoSaveInterval = 60 * 1000;
|
||||||
|
|
||||||
|
export const getLocalDocuments = (db, DocumentID) => {
|
||||||
|
try {
|
||||||
|
const transaction = db.transaction(["customers"], "readonly");
|
||||||
|
const store = transaction.objectStore("customers");
|
||||||
|
const keyRange = IDBKeyRange.only(DocumentID);
|
||||||
|
const index = store.index("DocumentID");
|
||||||
|
const req = index.openCursor(keyRange);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const result = [];
|
||||||
|
req.onsuccess = (event) => {
|
||||||
|
const cursor = event.target.result;
|
||||||
|
if (cursor) {
|
||||||
|
// Do something with the matches.
|
||||||
|
result.push(cursor.value);
|
||||||
|
cursor.continue();
|
||||||
|
} else {
|
||||||
|
// console.log('获取成功 ');
|
||||||
|
result.sort(saveTimeSort);
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.onerror = (event) => {
|
||||||
|
console.error("获取失败");
|
||||||
|
reject(event);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log("未知错误", DocumentID);
|
||||||
|
return Promise.reject(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setLocalDocuments = (db, localDocuments, document = {}) => {
|
||||||
|
const draftIndex = 0;
|
||||||
|
if (localDocuments[draftIndex + 1] && localDocuments[draftIndex + 1].Content === localDocuments[draftIndex].Content) {
|
||||||
|
// console.log("内容未更新,不进行本地保存。");
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(["customers"], "readwrite");
|
||||||
|
const store = transaction.objectStore("customers");
|
||||||
|
let req = {};
|
||||||
|
|
||||||
|
// Info: 长度超过用 put,没超过用 add
|
||||||
|
if (localDocuments.length >= MaxLocalDocumentLength) {
|
||||||
|
const {id} = localDocuments.sort(saveTimeSort)[localDocuments.length - 1];
|
||||||
|
req = store.put({
|
||||||
|
...document,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
req = store.add(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.onsuccess = () => {
|
||||||
|
// console.log("自动保存成功");
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
req.onerror = (event) => {
|
||||||
|
message.error("自动保存失败");
|
||||||
|
reject(event);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setLocalDraft = (db, localDocuments, document = {}) => {
|
||||||
|
const draft = localDocuments[0];
|
||||||
|
if (draft && document.Content === draft.Content) {
|
||||||
|
console.log("草稿未更新,不进行本地保存。");
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(["customers"], "readwrite");
|
||||||
|
const store = transaction.objectStore("customers");
|
||||||
|
|
||||||
|
// Info: 更新草稿
|
||||||
|
const {id} = draft;
|
||||||
|
const req = store.put({
|
||||||
|
...document,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
req.onsuccess = () => {
|
||||||
|
// console.log("自动保存草稿成功");
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
req.onerror = (event) => {
|
||||||
|
message.error("自动保存草稿失败");
|
||||||
|
reject(event);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
13
src/component/MenuLeft/CodeTheme.css
Normal file
13
src/component/MenuLeft/CodeTheme.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.nice-codetheme-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 5px 12px 5px 6px;
|
||||||
|
}
|
||||||
|
.nice-codetheme-item-name {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.nice-codetheme-item-flag {
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
66
src/component/MenuLeft/CodeTheme.js
Normal file
66
src/component/MenuLeft/CodeTheme.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {Menu, Dropdown} from "antd";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {CODE_OPTIONS, RIGHT_SYMBOL, IS_MAC_CODE} from "../../utils/constant";
|
||||||
|
import "./CodeTheme.css";
|
||||||
|
|
||||||
|
@inject("navbar")
|
||||||
|
@observer
|
||||||
|
class CodeTheme extends React.Component {
|
||||||
|
changeCodeTheme = (item) => {
|
||||||
|
// 是否为 Mac 风格代码
|
||||||
|
if (item.key === IS_MAC_CODE) {
|
||||||
|
const {isMacCode, codeNum} = this.props.navbar;
|
||||||
|
if (isMacCode) {
|
||||||
|
this.props.navbar.setMacCode(false);
|
||||||
|
this.props.navbar.setCodeNum(codeNum, false);
|
||||||
|
} else {
|
||||||
|
this.props.navbar.setMacCode(true);
|
||||||
|
this.props.navbar.setCodeNum(codeNum, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const {isMacCode} = this.props.navbar;
|
||||||
|
const codeNum = parseInt(item.key, 10);
|
||||||
|
this.props.navbar.setCodeNum(codeNum, isMacCode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {codeNum, isMacCode} = this.props.navbar;
|
||||||
|
|
||||||
|
const codeMenu = (
|
||||||
|
<Menu onClick={this.changeCodeTheme}>
|
||||||
|
{CODE_OPTIONS.map((option, index) => (
|
||||||
|
<Menu.Item key={index}>
|
||||||
|
<div id={`nice-menu-codetheme-${option.id}`} className="nice-codetheme-item">
|
||||||
|
<span>
|
||||||
|
<span className="nice-codetheme-item-flag">{codeNum === index && <span>{RIGHT_SYMBOL}</span>}</span>
|
||||||
|
<span className="nice-codetheme-item-name">{option.name}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Item key={IS_MAC_CODE}>
|
||||||
|
<div id="nice-menu-codetheme-apple" className="nice-codetheme-item">
|
||||||
|
<span>
|
||||||
|
<span className="nice-codetheme-item-flag">{isMacCode && <span>{RIGHT_SYMBOL}</span>}</span>
|
||||||
|
<span className="nice-codetheme-item-name">Mac 风格</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={codeMenu} trigger={["click"]} overlayClassName="nice-overlay">
|
||||||
|
<a id="nice-menu-codetheme" className="nice-menu-link" href="#">
|
||||||
|
代码主题
|
||||||
|
</a>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CodeTheme;
|
27
src/component/MenuLeft/File.js
Normal file
27
src/component/MenuLeft/File.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {Menu, Dropdown} from "antd";
|
||||||
|
|
||||||
|
import ImportFile from "./File/ImportFile";
|
||||||
|
import "./common.css";
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item>
|
||||||
|
<ImportFile />
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
class File extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={menu} trigger={["click"]} overlayClassName="nice-overlay">
|
||||||
|
<a id="nice-menu-file" className="nice-menu-link" href="#">
|
||||||
|
文件
|
||||||
|
</a>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default File;
|
40
src/component/MenuLeft/File/ImportFile.js
Normal file
40
src/component/MenuLeft/File/ImportFile.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
import {message} from "antd";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@observer
|
||||||
|
class ImportFile extends Component {
|
||||||
|
handleChange = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
this.props.content.setContent(event.target.result);
|
||||||
|
message.success("导入文件成功!");
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<label id="nice-menu-import-file" className="nice-menu-item" htmlFor="importFile">
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">导入</span>
|
||||||
|
<input
|
||||||
|
style={{display: "none"}}
|
||||||
|
type="file"
|
||||||
|
id="importFile"
|
||||||
|
accept=".txt,.md"
|
||||||
|
hidden=""
|
||||||
|
onChange={this.handleChange}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImportFile;
|
43
src/component/MenuLeft/Function.js
Normal file
43
src/component/MenuLeft/Function.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {Menu, Dropdown} from "antd";
|
||||||
|
|
||||||
|
import Reset from "./Function/Reset";
|
||||||
|
import Search from "./Function/Search";
|
||||||
|
import History from "./Function/History";
|
||||||
|
import SitDown from "./Function/SitDown";
|
||||||
|
|
||||||
|
import "./common.css";
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item>
|
||||||
|
<Reset />
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<Search />
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
|
<Menu.Divider />
|
||||||
|
|
||||||
|
<Menu.Item>
|
||||||
|
<History />
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<SitDown />
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
class Function extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={menu} trigger={["click"]} overlayClassName="nice-overlay">
|
||||||
|
<a id="nice-menu-function" className="nice-menu-link" href="#">
|
||||||
|
功能
|
||||||
|
</a>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Function;
|
25
src/component/MenuLeft/Function/History.js
Normal file
25
src/component/MenuLeft/Function/History.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class History extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
this.props.dialog.setHistoryOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-history" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">本地历史</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default History;
|
42
src/component/MenuLeft/Function/Reset.js
Normal file
42
src/component/MenuLeft/Function/Reset.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {Modal, message} from "antd";
|
||||||
|
|
||||||
|
import TEMPLATE from "../../../template/index";
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@inject("navbar")
|
||||||
|
@observer
|
||||||
|
class Reset extends Component {
|
||||||
|
showConfirm = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "确认重置么?",
|
||||||
|
content: "重置后将丢失本地保存的文本和自定义样式",
|
||||||
|
okText: "确定",
|
||||||
|
okType: "danger",
|
||||||
|
cancelText: "取消",
|
||||||
|
onOk: () => {
|
||||||
|
this.props.content.setContent(TEMPLATE.content);
|
||||||
|
this.props.content.setStyle(TEMPLATE.normal);
|
||||||
|
this.props.content.setCustomStyle(TEMPLATE.custom);
|
||||||
|
this.props.navbar.setTemplateNum(0);
|
||||||
|
message.success("重置成功!");
|
||||||
|
},
|
||||||
|
onCancel() {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-reset" className="nice-menu-item" onClick={this.showConfirm}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">重置</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Reset;
|
28
src/component/MenuLeft/Function/Search.js
Normal file
28
src/component/MenuLeft/Function/Search.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {hotKeys} from "../../../utils/hotkey";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class Search extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
this.props.dialog.setSearchOpen(!this.props.dialog.isSearchOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-search" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">查找</span>
|
||||||
|
</span>
|
||||||
|
<span className="nice-menu-shortcut">{hotKeys.search}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Search;
|
25
src/component/MenuLeft/Function/SitDown.js
Normal file
25
src/component/MenuLeft/Function/SitDown.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class SitDownFunction extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
this.props.dialog.setSitDownOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-sitdown" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">html转markdown</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SitDownFunction;
|
40
src/component/MenuLeft/Help.js
Normal file
40
src/component/MenuLeft/Help.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {Menu, Dropdown} from "antd";
|
||||||
|
|
||||||
|
import About from "./Help/About";
|
||||||
|
import Version from "./Help/Version";
|
||||||
|
import Document from "./Help/Document";
|
||||||
|
import Question from "./Help/Question";
|
||||||
|
|
||||||
|
import "./common.css";
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu>
|
||||||
|
{/*<Menu.Item>*/}
|
||||||
|
{/* <About />*/}
|
||||||
|
{/*</Menu.Item>*/}
|
||||||
|
{/*<Menu.Item>*/}
|
||||||
|
{/* <Version />*/}
|
||||||
|
{/*</Menu.Item>*/}
|
||||||
|
<Menu.Item>
|
||||||
|
<Document />
|
||||||
|
</Menu.Item>
|
||||||
|
{/*<Menu.Item>*/}
|
||||||
|
{/* <Question />*/}
|
||||||
|
{/*</Menu.Item>*/}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
class Help extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={menu} trigger={["click"]} overlayClassName="nice-overlay">
|
||||||
|
<a id="nice-menu-help" className="nice-menu-link" href="#">
|
||||||
|
帮助
|
||||||
|
</a>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Help;
|
25
src/component/MenuLeft/Help/About.js
Normal file
25
src/component/MenuLeft/Help/About.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class About extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
this.props.dialog.setAboutOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-about" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">关于</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default About;
|
23
src/component/MenuLeft/Help/Document.js
Normal file
23
src/component/MenuLeft/Help/Document.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
class Document extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const w = window.open("about:blank");
|
||||||
|
w.location.href = "https://doc.luckday.cn";
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-document" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">更多文档</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Document;
|
23
src/component/MenuLeft/Help/Question.js
Normal file
23
src/component/MenuLeft/Help/Question.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
class Question extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const w = window.open("about:blank");
|
||||||
|
w.location.href = "https://github.com/shenweiyan/Markdown2Html/issues/new";
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-question" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">我要提问</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Question;
|
25
src/component/MenuLeft/Help/Version.js
Normal file
25
src/component/MenuLeft/Help/Version.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class Version extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
this.props.dialog.setVersionOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-version" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">版本信息</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Version;
|
25
src/component/MenuLeft/Help/Version.js.default
Normal file
25
src/component/MenuLeft/Help/Version.js.default
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class Version extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
this.props.dialog.setVersionOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-version" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">版本信息</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Version;
|
23
src/component/MenuLeft/Help/Version.js.new
Normal file
23
src/component/MenuLeft/Help/Version.js.new
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
class Question extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const w = window.open("about:blank");
|
||||||
|
w.location.href = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-question" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">版本信息</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Version;
|
60
src/component/MenuLeft/LogIn.js
Normal file
60
src/component/MenuLeft/LogIn.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {Tooltip, Button} from "antd";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import {CLIENT_ID, CLIENT_SECRET, PROXY, ACCESS_TOKEN, ENTER_DELAY, LEAVE_DELAY} from "../../utils/constant";
|
||||||
|
import {queryParse, axiosJSON, axiosGithub} from "../../utils/helper";
|
||||||
|
import SvgIcon from "../../icon";
|
||||||
|
|
||||||
|
@inject("userInfo")
|
||||||
|
@observer
|
||||||
|
class LogIn extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.code = queryParse().code;
|
||||||
|
if (this.code) {
|
||||||
|
this.loginBack();
|
||||||
|
}
|
||||||
|
const TOKEN = localStorage.getItem(ACCESS_TOKEN);
|
||||||
|
if (TOKEN) {
|
||||||
|
axios.defaults.headers.common.Authorization = `token ${TOKEN}`;
|
||||||
|
this.getUserInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
login = () => {
|
||||||
|
window.location.href = `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&scope=public_repo`;
|
||||||
|
};
|
||||||
|
|
||||||
|
loginBack = async () => {
|
||||||
|
const res = await axiosJSON.post(PROXY, {
|
||||||
|
code: this.code,
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
client_secret: CLIENT_SECRET,
|
||||||
|
});
|
||||||
|
localStorage.setItem(ACCESS_TOKEN, res.data.access_token);
|
||||||
|
window.location.href = "/";
|
||||||
|
};
|
||||||
|
|
||||||
|
getUserInfo = async () => {
|
||||||
|
try {
|
||||||
|
const res = await axiosGithub.get(`/user`);
|
||||||
|
this.props.userInfo.setUserInfo(res.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Tooltip placement="bottom" mouseEnterDelay={ENTER_DELAY} mouseLeaveDelay={LEAVE_DELAY} title="登录">
|
||||||
|
<Button shape="circle" className="nice-btn-login" onClick={this.login}>
|
||||||
|
<SvgIcon name="github" className="nice-btn-login-icon" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LogIn;
|
10
src/component/MenuLeft/Login.css
Normal file
10
src/component/MenuLeft/Login.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.nice-btn-login {
|
||||||
|
border: none;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-btn-login-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
38
src/component/MenuLeft/Paragraph.js
Normal file
38
src/component/MenuLeft/Paragraph.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {Menu, Dropdown} from "antd";
|
||||||
|
|
||||||
|
import "./common.css";
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item>
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href="#">
|
||||||
|
导出
|
||||||
|
</a>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href="#">
|
||||||
|
导入
|
||||||
|
</a>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href="#">
|
||||||
|
打印
|
||||||
|
</a>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
class Paragraph extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={menu} trigger={["click"]} overlayClassName="nice-overlay">
|
||||||
|
<a className="nice-menu-link" href="#">
|
||||||
|
段落
|
||||||
|
</a>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Paragraph;
|
74
src/component/MenuLeft/Pattern.js
Normal file
74
src/component/MenuLeft/Pattern.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {Menu, Dropdown} from "antd";
|
||||||
|
|
||||||
|
import Bold from "./Pattern/Bold";
|
||||||
|
import Code from "./Pattern/Code";
|
||||||
|
import Del from "./Pattern/Del";
|
||||||
|
import Italic from "./Pattern/Italic";
|
||||||
|
import Link from "./Pattern/Link";
|
||||||
|
import Form from "./Pattern/Form";
|
||||||
|
import Image from "./Pattern/Image";
|
||||||
|
import Format from "./Pattern/Format";
|
||||||
|
import LinkToFoot from "./Pattern/LinkToFoot";
|
||||||
|
import Font from "./Pattern/Font";
|
||||||
|
import InlineCode from "./Pattern/InlineCode";
|
||||||
|
|
||||||
|
import "./common.css";
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item>
|
||||||
|
<Del />
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<Bold />
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<Italic />
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<Code />
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<InlineCode />
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
|
<Menu.Divider />
|
||||||
|
|
||||||
|
<Menu.Item>
|
||||||
|
<Link />
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<Form />
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<Image />
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<Font />
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
|
<Menu.Divider />
|
||||||
|
|
||||||
|
<Menu.Item>
|
||||||
|
<LinkToFoot />
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<Format />
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
class Pattern extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={menu} trigger={["click"]} overlayClassName="nice-overlay">
|
||||||
|
<a id="nice-menu-pattern" className="nice-menu-link" href="#">
|
||||||
|
格式
|
||||||
|
</a>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Pattern;
|
37
src/component/MenuLeft/Pattern/Bold.js
Normal file
37
src/component/MenuLeft/Pattern/Bold.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {bold} from "../../../utils/editorKeyEvents";
|
||||||
|
import {hotKeys} from "../../../utils/hotkey";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@observer
|
||||||
|
class Bold extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const selection = markdownEditor.getSelection();
|
||||||
|
bold(markdownEditor, selection);
|
||||||
|
|
||||||
|
// 上传后实时更新内容
|
||||||
|
const content = markdownEditor.getValue();
|
||||||
|
this.props.content.setContent(content);
|
||||||
|
markdownEditor.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-bold" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">加粗</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="nice-menu-shortcut">{hotKeys.bold}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Bold;
|
36
src/component/MenuLeft/Pattern/Code.js
Normal file
36
src/component/MenuLeft/Pattern/Code.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {code} from "../../../utils/editorKeyEvents";
|
||||||
|
import {hotKeys} from "../../../utils/hotkey";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@observer
|
||||||
|
class Code extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const selection = markdownEditor.getSelection();
|
||||||
|
code(markdownEditor, selection);
|
||||||
|
|
||||||
|
// 上传后实时更新内容
|
||||||
|
const content = markdownEditor.getValue();
|
||||||
|
this.props.content.setContent(content);
|
||||||
|
markdownEditor.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-code" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">代码</span>
|
||||||
|
</span>
|
||||||
|
<span className="nice-menu-shortcut">{hotKeys.code}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Code;
|
36
src/component/MenuLeft/Pattern/Del.js
Normal file
36
src/component/MenuLeft/Pattern/Del.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {del} from "../../../utils/editorKeyEvents";
|
||||||
|
import {hotKeys} from "../../../utils/hotkey";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@observer
|
||||||
|
class Del extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const selection = markdownEditor.getSelection();
|
||||||
|
del(markdownEditor, selection);
|
||||||
|
|
||||||
|
// 上传后实时更新内容
|
||||||
|
const content = markdownEditor.getValue();
|
||||||
|
this.props.content.setContent(content);
|
||||||
|
markdownEditor.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-del" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">删除线</span>
|
||||||
|
</span>
|
||||||
|
<span className="nice-menu-shortcut">{hotKeys.del}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Del;
|
43
src/component/MenuLeft/Pattern/Font.js
Normal file
43
src/component/MenuLeft/Pattern/Font.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {message} from "antd";
|
||||||
|
|
||||||
|
import {FONT_THEME_ID, RIGHT_SYMBOL} from "../../../utils/constant";
|
||||||
|
import {replaceStyle} from "../../../utils/helper";
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
class Font extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isSerif: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 衬线字体 和 非衬线字体 切换
|
||||||
|
toggleFont = () => {
|
||||||
|
const {isSerif} = this.state;
|
||||||
|
const serif = `#nice {
|
||||||
|
font-family: Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
|
||||||
|
}`;
|
||||||
|
const sansSerif = `#nice {
|
||||||
|
font-family: Roboto, Oxygen, Ubuntu, Cantarell, PingFangSC-light, PingFangTC-light, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
}`;
|
||||||
|
const choosen = isSerif ? serif : sansSerif;
|
||||||
|
replaceStyle(FONT_THEME_ID, choosen);
|
||||||
|
message.success("字体切换成功!");
|
||||||
|
this.setState({isSerif: !isSerif});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-font" className="nice-menu-item" onClick={this.toggleFont}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag">{!this.state.isSerif && <span>{RIGHT_SYMBOL}</span>}</span>
|
||||||
|
<span className="nice-menu-name">衬线字体</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Font;
|
28
src/component/MenuLeft/Pattern/Form.js
Normal file
28
src/component/MenuLeft/Pattern/Form.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {inject, observer} from "mobx-react";
|
||||||
|
|
||||||
|
import {hotKeys} from "../../../utils/hotkey";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class Form extends Component {
|
||||||
|
showModal = () => {
|
||||||
|
this.props.dialog.setFormOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-form" className="nice-menu-item" onClick={this.showModal}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">表格</span>
|
||||||
|
</span>
|
||||||
|
<span className="nice-menu-shortcut">{hotKeys.form}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Form;
|
30
src/component/MenuLeft/Pattern/Format.js
Normal file
30
src/component/MenuLeft/Pattern/Format.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {formatDoc} from "../../../utils/editorKeyEvents";
|
||||||
|
import {hotKeys} from "../../../utils/hotkey";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@observer
|
||||||
|
class Format extends Component {
|
||||||
|
handleFormat = () => {
|
||||||
|
const {content} = this.props.content;
|
||||||
|
formatDoc(content, this.props.content);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-format" className="nice-menu-item" onClick={this.handleFormat}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">格式化文档</span>
|
||||||
|
</span>
|
||||||
|
<span className="nice-menu-shortcut">{hotKeys.format}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Format;
|
28
src/component/MenuLeft/Pattern/Image.js
Normal file
28
src/component/MenuLeft/Pattern/Image.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {hotKeys} from "../../../utils/hotkey";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class Image extends Component {
|
||||||
|
showModal = () => {
|
||||||
|
this.props.dialog.setImageOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-image" className="nice-menu-item" onClick={this.showModal}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">图片</span>
|
||||||
|
</span>
|
||||||
|
<span className="nice-menu-shortcut">{hotKeys.image}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Image;
|
36
src/component/MenuLeft/Pattern/InlineCode.js
Normal file
36
src/component/MenuLeft/Pattern/InlineCode.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {inlineCode} from "../../../utils/editorKeyEvents";
|
||||||
|
import {hotKeys} from "../../../utils/hotkey";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@observer
|
||||||
|
class InlineCode extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const selection = markdownEditor.getSelection();
|
||||||
|
inlineCode(markdownEditor, selection);
|
||||||
|
|
||||||
|
// 上传后实时更新内容
|
||||||
|
const content = markdownEditor.getValue();
|
||||||
|
this.props.content.setContent(content);
|
||||||
|
markdownEditor.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-inline-code" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">行内代码</span>
|
||||||
|
</span>
|
||||||
|
<span className="nice-menu-shortcut">{hotKeys.inlineCode}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InlineCode;
|
36
src/component/MenuLeft/Pattern/Italic.js
Normal file
36
src/component/MenuLeft/Pattern/Italic.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {italic} from "../../../utils/editorKeyEvents";
|
||||||
|
import {hotKeys} from "../../../utils/hotkey";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@observer
|
||||||
|
class Italic extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const selection = markdownEditor.getSelection();
|
||||||
|
italic(markdownEditor, selection);
|
||||||
|
|
||||||
|
// 上传后实时更新内容
|
||||||
|
const content = markdownEditor.getValue();
|
||||||
|
this.props.content.setContent(content);
|
||||||
|
markdownEditor.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-italic" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">倾斜</span>
|
||||||
|
</span>
|
||||||
|
<span className="nice-menu-shortcut">{hotKeys.italic}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Italic;
|
28
src/component/MenuLeft/Pattern/Link.js
Normal file
28
src/component/MenuLeft/Pattern/Link.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {hotKeys} from "../../../utils/hotkey";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class Link extends Component {
|
||||||
|
showModal = () => {
|
||||||
|
this.props.dialog.setLinkOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-link" className="nice-menu-item" onClick={this.showModal}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">链接</span>
|
||||||
|
</span>
|
||||||
|
<span className="nice-menu-shortcut">{hotKeys.link}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Link;
|
30
src/component/MenuLeft/Pattern/LinkToFoot.js
Normal file
30
src/component/MenuLeft/Pattern/LinkToFoot.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {parseLinkToFoot} from "../../../utils/editorKeyEvents";
|
||||||
|
import {hotKeys} from "../../../utils/hotkey";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@observer
|
||||||
|
class Format extends Component {
|
||||||
|
handleFormat = () => {
|
||||||
|
const {content} = this.props.content;
|
||||||
|
parseLinkToFoot(content, this.props.content);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-link-to-foot" className="nice-menu-item" onClick={this.handleFormat}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">微信外链转脚注</span>
|
||||||
|
</span>
|
||||||
|
<span className="nice-menu-shortcut">{hotKeys.linkToFoot}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Format;
|
32
src/component/MenuLeft/Setting.js
Normal file
32
src/component/MenuLeft/Setting.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {Menu, Dropdown} from "antd";
|
||||||
|
|
||||||
|
import SyncScroll from "./Setting/SyncScroll";
|
||||||
|
import ContainImgName from "./Setting/ContainImgName";
|
||||||
|
|
||||||
|
import "./common.css";
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item>
|
||||||
|
<SyncScroll />
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<ContainImgName />
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
class Setting extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={menu} trigger={["click"]} overlayClassName="nice-overlay">
|
||||||
|
<a id="nice-menu-setting" className="nice-menu-link" href="#">
|
||||||
|
设置
|
||||||
|
</a>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Setting;
|
27
src/component/MenuLeft/Setting/ContainImgName.js
Normal file
27
src/component/MenuLeft/Setting/ContainImgName.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {RIGHT_SYMBOL} from "../../../utils/constant";
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("navbar")
|
||||||
|
@observer
|
||||||
|
class SyncScroll extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const {isContainImgName} = this.props.navbar;
|
||||||
|
this.props.navbar.setContainImgName(!isContainImgName);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-contain-img-name" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag">{this.props.navbar.isContainImgName && <span>{RIGHT_SYMBOL}</span>}</span>
|
||||||
|
<span className="nice-menu-name">上传图片时包含名称</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SyncScroll;
|
27
src/component/MenuLeft/Setting/SyncScroll.js
Normal file
27
src/component/MenuLeft/Setting/SyncScroll.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {RIGHT_SYMBOL} from "../../../utils/constant";
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("navbar")
|
||||||
|
@observer
|
||||||
|
class SyncScroll extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const {isSyncScroll} = this.props.navbar;
|
||||||
|
this.props.navbar.setSyncScroll(!isSyncScroll);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-sync-scroll" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag">{this.props.navbar.isSyncScroll && <span>{RIGHT_SYMBOL}</span>}</span>
|
||||||
|
<span className="nice-menu-name">同步滚动</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SyncScroll;
|
54
src/component/MenuLeft/Theme.css
Normal file
54
src/component/MenuLeft/Theme.css
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
.nice-themeselect-md-cutom-menu {
|
||||||
|
margin-left: 8px;
|
||||||
|
border: 1px dashed #1890ff;
|
||||||
|
}
|
||||||
|
.nice-themeselect-md-menu {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.nice-themeselect-code-menu {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.nice-themeselect-theme-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 5px 12px 5px 6px;
|
||||||
|
}
|
||||||
|
.nice-themeselect-theme-item-author {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
.nice-themeselect-theme-item-name {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.nice-themeselect-theme-item-new {
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #1890ff;
|
||||||
|
background: rgb(230, 247, 255);
|
||||||
|
padding: 0px 5px;
|
||||||
|
margin-left: -5px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
.nice-themeselect-theme-item-flag {
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.nice-themeselect-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
.nice-themeselect-menu-item {
|
||||||
|
clear: both;
|
||||||
|
margin: 0;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-menu-subscribe-more {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
128
src/component/MenuLeft/Theme.js
Normal file
128
src/component/MenuLeft/Theme.js
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {Menu, Dropdown} from "antd";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {RIGHT_SYMBOL, TEMPLATE_NUM, MARKDOWN_THEME_ID, STYLE} from "../../utils/constant";
|
||||||
|
import {replaceStyle} from "../../utils/helper";
|
||||||
|
import TEMPLATE from "../../template/index";
|
||||||
|
import "./Theme.css";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@inject("navbar")
|
||||||
|
@inject("view")
|
||||||
|
@observer
|
||||||
|
class Theme extends React.Component {
|
||||||
|
changeTemplate = (item) => {
|
||||||
|
const index = parseInt(item.key, 10);
|
||||||
|
const {themeId, css} = this.props.content.themeList[index];
|
||||||
|
this.props.navbar.setTemplateNum(index);
|
||||||
|
|
||||||
|
// 更新style编辑器
|
||||||
|
if (themeId === "custom") {
|
||||||
|
this.props.content.setCustomStyle();
|
||||||
|
// 切换自定义自动打开css编辑
|
||||||
|
this.props.view.setStyleEditorOpen(true);
|
||||||
|
} else {
|
||||||
|
this.props.content.setStyle(css);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleStyleEditor = () => {
|
||||||
|
const {isStyleEditorOpen} = this.props.view;
|
||||||
|
this.props.view.setStyleEditorOpen(!isStyleEditorOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
subscribeMore = () => {
|
||||||
|
const w = window.open("about:blank");
|
||||||
|
w.location.href = "https://preview.mdnice.com/themes";
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount = async () => {
|
||||||
|
const themeList = [
|
||||||
|
{themeId: "normal", name: "默认主题", css: TEMPLATE.theme.normal},
|
||||||
|
{themeId: "1", name: "橙心", css: TEMPLATE.theme.one},
|
||||||
|
{themeId: "2", name: "姹紫", css: TEMPLATE.theme.two},
|
||||||
|
{themeId: "3", name: "嫩青", css: TEMPLATE.theme.three},
|
||||||
|
{themeId: "4", name: "绿意", css: TEMPLATE.theme.four},
|
||||||
|
{themeId: "5", name: "红绯", css: TEMPLATE.theme.five},
|
||||||
|
{themeId: "6", name: "蓝莹", css: TEMPLATE.theme.six},
|
||||||
|
{themeId: "7", name: "兰青", css: TEMPLATE.theme.seven},
|
||||||
|
{themeId: "8", name: "山吹", css: TEMPLATE.theme.eight},
|
||||||
|
{themeId: "9", name: "网格黑", css: TEMPLATE.theme.wgh},
|
||||||
|
{themeId: "10", name: "极客黑", css: TEMPLATE.theme.ten},
|
||||||
|
{themeId: "11", name: "蔷薇紫", css: TEMPLATE.theme.eleven},
|
||||||
|
{themeId: "12", name: "萌绿风", css: TEMPLATE.theme.twelve},
|
||||||
|
{themeId: "13", name: "全栈蓝", css: TEMPLATE.theme.thirteen},
|
||||||
|
{themeId: "14", name: "极简黑", css: TEMPLATE.theme.fourteen},
|
||||||
|
{themeId: "15", name: "橙蓝风", css: TEMPLATE.theme.fifteen},
|
||||||
|
{themeId: "16", name: "前端之巅同款", css: TEMPLATE.theme.nine},
|
||||||
|
{themeId: "custom", name: "自定义", css: TEMPLATE.theme.custom},
|
||||||
|
];
|
||||||
|
|
||||||
|
this.props.content.setThemeList(themeList);
|
||||||
|
// 设置一下自定义的规则
|
||||||
|
if (!window.localStorage.getItem(STYLE)) {
|
||||||
|
window.localStorage.setItem(STYLE, TEMPLATE.theme.custom);
|
||||||
|
}
|
||||||
|
const templateNum = parseInt(window.localStorage.getItem(TEMPLATE_NUM), 10);
|
||||||
|
|
||||||
|
// 主题样式初始化,属于自定义主题则从localstorage中读数据
|
||||||
|
let style = "";
|
||||||
|
if (templateNum === themeList.length - 1) {
|
||||||
|
style = window.localStorage.getItem(STYLE);
|
||||||
|
} else {
|
||||||
|
if (templateNum) {
|
||||||
|
const {css} = themeList[templateNum];
|
||||||
|
style = css;
|
||||||
|
} else {
|
||||||
|
style = TEMPLATE.normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.props.content.setStyle(style);
|
||||||
|
replaceStyle(MARKDOWN_THEME_ID, style);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {templateNum} = this.props.navbar;
|
||||||
|
const {themeList} = this.props.content;
|
||||||
|
|
||||||
|
const mdMenu = (
|
||||||
|
<Menu onClick={this.changeTemplate}>
|
||||||
|
{themeList.map((option, index) => (
|
||||||
|
<Menu.Item key={index}>
|
||||||
|
<div id={`nice-menu-theme-${option.themeId}`} className="nice-themeselect-theme-item">
|
||||||
|
<span>
|
||||||
|
<span className="nice-themeselect-theme-item-flag">
|
||||||
|
{templateNum === index && <span>{RIGHT_SYMBOL}</span>}
|
||||||
|
</span>
|
||||||
|
<span className="nice-themeselect-theme-item-name">{option.name}</span>
|
||||||
|
{option.isNew && <span className="nice-themeselect-theme-item-new">new</span>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
<Menu.Divider />
|
||||||
|
<li className="nice-themeselect-menu-item">
|
||||||
|
<div id="nice-menu-view-css" className="nice-themeselect-theme-item" onClick={this.toggleStyleEditor}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-themeselect-theme-item-flag">
|
||||||
|
{this.props.view.isStyleEditorOpen && <span>{RIGHT_SYMBOL}</span>}
|
||||||
|
</span>
|
||||||
|
<span className="nice-themeselect-theme-item-name">查看主题 CSS</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={mdMenu} trigger={["click"]} overlayClassName="nice-overlay">
|
||||||
|
<a id="nice-menu-theme" className="nice-menu-link" href="#">
|
||||||
|
主题
|
||||||
|
</a>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Theme;
|
43
src/component/MenuLeft/View.js
Normal file
43
src/component/MenuLeft/View.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {Menu, Dropdown} from "antd";
|
||||||
|
|
||||||
|
import FullScreen from "./View/FullScreen";
|
||||||
|
import EditArea from "./View/EditArea";
|
||||||
|
import PreviewArea from "./View/PreviewArea";
|
||||||
|
import ThemeArea from "./View/ThemeArea";
|
||||||
|
|
||||||
|
import "./common.css";
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item>
|
||||||
|
<FullScreen />
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
|
<Menu.Divider />
|
||||||
|
|
||||||
|
<Menu.Item>
|
||||||
|
<EditArea />
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<PreviewArea />
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<ThemeArea />
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
class View extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={menu} trigger={["click"]} overlayClassName="nice-overlay">
|
||||||
|
<a id="nice-menu-view" className="nice-menu-link" href="#">
|
||||||
|
查看
|
||||||
|
</a>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default View;
|
27
src/component/MenuLeft/View/EditArea.js
Normal file
27
src/component/MenuLeft/View/EditArea.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {RIGHT_SYMBOL} from "../../../utils/constant";
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("view")
|
||||||
|
@observer
|
||||||
|
class EditArea extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const {isEditAreaOpen} = this.props.view;
|
||||||
|
this.props.view.setEditAreaOpen(!isEditAreaOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-edit-area" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag">{this.props.view.isEditAreaOpen && <span>{RIGHT_SYMBOL}</span>}</span>
|
||||||
|
<span className="nice-menu-name">编辑区域</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditArea;
|
46
src/component/MenuLeft/View/FullScreen.js
Normal file
46
src/component/MenuLeft/View/FullScreen.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("navbar")
|
||||||
|
@observer
|
||||||
|
class FullScreen extends Component {
|
||||||
|
// fullScreen or !fullScreen
|
||||||
|
toggleFullScreen = () => {
|
||||||
|
const doc = window.document;
|
||||||
|
const docEl = doc.documentElement;
|
||||||
|
|
||||||
|
const requestFullScreen =
|
||||||
|
docEl.requestFullscreen ||
|
||||||
|
docEl.mozRequestFullScreen ||
|
||||||
|
docEl.webkitRequestFullScreen ||
|
||||||
|
docEl.msRequestFullscreen;
|
||||||
|
const cancelFullScreen =
|
||||||
|
doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!doc.fullscreenElement &&
|
||||||
|
!doc.mozFullScreenElement &&
|
||||||
|
!doc.webkitFullscreenElement &&
|
||||||
|
!doc.msFullscreenElement
|
||||||
|
) {
|
||||||
|
requestFullScreen.call(docEl);
|
||||||
|
} else {
|
||||||
|
cancelFullScreen.call(doc);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-full-screen" className="nice-menu-item" onClick={this.toggleFullScreen}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag" />
|
||||||
|
<span className="nice-menu-name">全屏</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FullScreen;
|
27
src/component/MenuLeft/View/PreviewArea.js
Normal file
27
src/component/MenuLeft/View/PreviewArea.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {RIGHT_SYMBOL} from "../../../utils/constant";
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("view")
|
||||||
|
@observer
|
||||||
|
class PreviewArea extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const {isPreviewAreaOpen} = this.props.view;
|
||||||
|
this.props.view.setPreviewAreaOpen(!isPreviewAreaOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-preview-area" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag">{this.props.view.isPreviewAreaOpen && <span>{RIGHT_SYMBOL}</span>}</span>
|
||||||
|
<span className="nice-menu-name">预览区域</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PreviewArea;
|
27
src/component/MenuLeft/View/ThemeArea.js
Normal file
27
src/component/MenuLeft/View/ThemeArea.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import {RIGHT_SYMBOL} from "../../../utils/constant";
|
||||||
|
import "../common.css";
|
||||||
|
|
||||||
|
@inject("view")
|
||||||
|
@observer
|
||||||
|
class ThemeArea extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const {isStyleEditorOpen} = this.props.view;
|
||||||
|
this.props.view.setStyleEditorOpen(!isStyleEditorOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="nice-menu-theme-area" className="nice-menu-item" onClick={this.handleClick}>
|
||||||
|
<span>
|
||||||
|
<span className="nice-menu-flag">{this.props.view.isStyleEditorOpen && <span>{RIGHT_SYMBOL}</span>}</span>
|
||||||
|
<span className="nice-menu-name">主题CSS区域</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThemeArea;
|
65
src/component/MenuLeft/common.css
Normal file
65
src/component/MenuLeft/common.css
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
.nice-menu-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px 12px 5px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-menu-shortcut {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-menu-name {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-menu-flag {
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-menu-link {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-menu-link:hover {
|
||||||
|
color: #cecece;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-menu-link:focus {
|
||||||
|
color: #cecece;
|
||||||
|
/*background: #e6f7ff;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-overlay {
|
||||||
|
top: 56px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-overlay ul {
|
||||||
|
border-radius: 0px 0px 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-overlay ul .ant-dropdown-menu-item {
|
||||||
|
clear: both;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-overlay ul label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.ant-dropdown-menu {
|
||||||
|
padding: 4px !important;
|
||||||
|
}
|
||||||
|
.ant-dropdown-menu-item:hover, .ant-dropdown-menu-submenu-title:hover {
|
||||||
|
background-color: #e8e8e8 !important;
|
||||||
|
}
|
109
src/component/SearchBox/SearchBox.css
Normal file
109
src/component/SearchBox/SearchBox.css
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
.mdnice-searchbox {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 20px;
|
||||||
|
z-index: 20;
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
|
padding: 8px;
|
||||||
|
max-width: calc(100% - 40px);
|
||||||
|
background-color: white;
|
||||||
|
height: 72px;
|
||||||
|
box-shadow: 0px 5px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdnice-searchbox-replace {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-left: 27px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdnice-searchbox > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdnice-searchbox div>:not(:last-child ) {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdnice-searchbox div input {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdnice-searchbox .searchbox-button {
|
||||||
|
background-color: white;
|
||||||
|
outline: none;
|
||||||
|
padding: 0px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdnice-searchbox .searchbox-button:active{
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbox-icon {
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbox-icon-casefold {
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbox-icon-prev {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbox-icon-replace {
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbox-icon-fold {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdnice-searchbox[data-replace="true"] .searchbox-icon-fold{
|
||||||
|
transform: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbox-text-highlight {
|
||||||
|
background-color: #91d5ff;
|
||||||
|
line-height: 16px;
|
||||||
|
padding: 3px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .mdnice-searchbox[data-active="false"] {
|
||||||
|
max-height: 0px !important;
|
||||||
|
padding-top: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.mdnice-searchbox[data-replace="false"] {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdnice-searchbox[data-replace="false"] .mdnice-searchbox-replace {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
.mdnice-searchbox ~ div .CodeMirror-sizer {
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.mdnice-searchbox ~ div .CodeMirror-sizer {
|
||||||
|
top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdnice-searchbox[data-replace="true"] ~ div .CodeMirror-sizer {
|
||||||
|
top: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
219
src/component/SearchBox/index.js
Normal file
219
src/component/SearchBox/index.js
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
/* eslint-disable react/no-did-update-set-state */
|
||||||
|
/* eslint-disable react/no-unused-state */
|
||||||
|
import React, {createRef} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {Input, Tooltip} from "antd";
|
||||||
|
|
||||||
|
import {ENTER_DELAY, LEAVE_DELAY} from "../../utils/constant";
|
||||||
|
|
||||||
|
import SvgIcon from "../../icon";
|
||||||
|
|
||||||
|
import "./SearchBox.css";
|
||||||
|
|
||||||
|
function WrappedButton(props) {
|
||||||
|
const className = props.className === undefined ? "" : props.className;
|
||||||
|
return (
|
||||||
|
<Tooltip placement="bottom" mouseEnterDelay={ENTER_DELAY} mouseLeaveDelay={LEAVE_DELAY} title={props.tipText}>
|
||||||
|
<button className="searchbox-button" type="button" onClick={props.onClick}>
|
||||||
|
<SvgIcon name={props.icon} className={`searchbox-icon ${className}`} fill={props.fill} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class SearchBox extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
replaceText: "",
|
||||||
|
searchText: "",
|
||||||
|
isReplaceOpen: false,
|
||||||
|
cursor: null,
|
||||||
|
caseFold: true,
|
||||||
|
};
|
||||||
|
this.searchRef = createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** false means next, true means previous */
|
||||||
|
posChange = (direction) => {
|
||||||
|
this.clearMarks();
|
||||||
|
if (typeof direction !== "boolean") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {cursor, searchText} = this.state;
|
||||||
|
if (searchText && cursor) {
|
||||||
|
cursor.find(direction);
|
||||||
|
if (cursor.atOccurrence) {
|
||||||
|
this.highlight();
|
||||||
|
} else {
|
||||||
|
while (cursor.find(!direction)) {
|
||||||
|
// 从头开始寻找
|
||||||
|
}
|
||||||
|
cursor.find(direction);
|
||||||
|
this.highlight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCaseFold = () => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
this.clearMarks();
|
||||||
|
this.setState(
|
||||||
|
(prevState) => {
|
||||||
|
const caseFold = !prevState.caseFold;
|
||||||
|
const cursor = prevState.searchText
|
||||||
|
? markdownEditor.getSearchCursor(prevState.searchText, null, {caseFold: caseFold})
|
||||||
|
: null;
|
||||||
|
return {caseFold, cursor};
|
||||||
|
},
|
||||||
|
() => this.posChange(false),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleScroll = (offset) => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const {top} = markdownEditor.getScrollInfo(offset);
|
||||||
|
console.log(offset);
|
||||||
|
markdownEditor.scrollTo(null, top + offset);
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillUnmount = () => {
|
||||||
|
this.handleScroll(this.state.isReplaceOpen ? -72 : -40);
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
this.searchRef.current.focus();
|
||||||
|
this.handleScroll(40);
|
||||||
|
};
|
||||||
|
|
||||||
|
clearMarks = () => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
// const markers = markdownEditor.getAllMarks();
|
||||||
|
// markers.forEach((marker) => marker.clear());
|
||||||
|
const cursor = markdownEditor.getCursor();
|
||||||
|
markdownEditor.setSelection(cursor);
|
||||||
|
};
|
||||||
|
|
||||||
|
findContent = (value) => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
this.setState(
|
||||||
|
(prevState) => {
|
||||||
|
const cursor = value ? markdownEditor.getSearchCursor(value, null, {caseFold: prevState.caseFold}) : null;
|
||||||
|
return {searchText: value, cursor};
|
||||||
|
},
|
||||||
|
() => this.posChange(false),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
highlight = () => {
|
||||||
|
// 高亮前需检测是否有匹配
|
||||||
|
if (this.state.cursor.atOccurrence) {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const from = this.state.cursor.from();
|
||||||
|
const to = this.state.cursor.to();
|
||||||
|
|
||||||
|
// markdownEditor.markText(from, to, {
|
||||||
|
// className: "searchbox-text-highlight",
|
||||||
|
// });
|
||||||
|
markdownEditor.setSelection(from, to);
|
||||||
|
// 防止搜索框挡住文字
|
||||||
|
markdownEditor.scrollIntoView(from, 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
replace = () => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const selection = markdownEditor.getSelection();
|
||||||
|
if (selection) {
|
||||||
|
// 未选中不进行替换
|
||||||
|
markdownEditor.replaceSelection(this.state.replaceText);
|
||||||
|
|
||||||
|
const content = markdownEditor.getValue();
|
||||||
|
this.props.content.setContent(content);
|
||||||
|
this.posChange(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
replaceAll = () => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const selection = markdownEditor.getSelection();
|
||||||
|
if (selection && this.state.searchText) {
|
||||||
|
const content = markdownEditor.getValue();
|
||||||
|
const searchReg = new RegExp(this.state.searchText, "g");
|
||||||
|
const replaced = content.replace(searchReg, this.state.replaceText);
|
||||||
|
|
||||||
|
this.props.content.setContent(replaced);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handelFoldClick = () => {
|
||||||
|
this.setState((prevState) => {
|
||||||
|
const {isReplaceOpen} = prevState;
|
||||||
|
this.handleScroll(isReplaceOpen ? -32 : 32);
|
||||||
|
return {isReplaceOpen: !isReplaceOpen};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClose = () => {
|
||||||
|
this.props.dialog.setSearchOpen(false);
|
||||||
|
this.clearMarks();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {isReplaceOpen} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-replace={isReplaceOpen} className="mdnice-searchbox">
|
||||||
|
<div>
|
||||||
|
<WrappedButton icon="down" tipText="展开" onClick={this.handelFoldClick} className="searchbox-icon-fold" />
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
value={this.state.searchText}
|
||||||
|
placeholder="按Enter进行查找"
|
||||||
|
onChange={(e) => this.findContent(e.target.value)}
|
||||||
|
onPressEnter={() => this.posChange(false)}
|
||||||
|
ref={this.searchRef}
|
||||||
|
/>
|
||||||
|
<WrappedButton
|
||||||
|
icon="fontCase"
|
||||||
|
onClick={this.handleCaseFold}
|
||||||
|
tipText="忽略大小写"
|
||||||
|
className="searchbox-icon-casefold"
|
||||||
|
fill={this.state.caseFold ? "red" : undefined}
|
||||||
|
/>
|
||||||
|
<WrappedButton
|
||||||
|
icon="down"
|
||||||
|
className="searchbox-icon-prev"
|
||||||
|
onClick={() => this.posChange(true)}
|
||||||
|
tipText="上一个"
|
||||||
|
/>
|
||||||
|
<WrappedButton icon="down" onClick={() => this.posChange(false)} tipText="下一个" />
|
||||||
|
<WrappedButton icon="close" onClick={this.handleClose} tipText="关闭" />
|
||||||
|
</div>
|
||||||
|
<div className="mdnice-searchbox-replace">
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
value={this.state.replaceText}
|
||||||
|
placeholder="按Enter进行替换"
|
||||||
|
onChange={(e) => {
|
||||||
|
this.setState({replaceText: e.target.value});
|
||||||
|
}}
|
||||||
|
onPressEnter={this.replace}
|
||||||
|
/>
|
||||||
|
<WrappedButton icon="replace" className="searchbox-icon-replace" onClick={this.replace} tipText="替换" />
|
||||||
|
<WrappedButton
|
||||||
|
icon="replaceAll"
|
||||||
|
className="searchbox-icon-replace"
|
||||||
|
onClick={this.replaceAll}
|
||||||
|
tipText="替换所有"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchBox;
|
32
src/component/Sidebar/ExportPdf.js
Normal file
32
src/component/Sidebar/ExportPdf.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {Tooltip} from "antd";
|
||||||
|
import React, {Component} from "react";
|
||||||
|
import SvgIcon from "../../icon";
|
||||||
|
import {ENTER_DELAY, LEAVE_DELAY} from "../../utils/constant";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
|
||||||
|
import "./pdf.css";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@inject("navbar")
|
||||||
|
@inject("imageHosting")
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class Pdf extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.print();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Tooltip placement="left" mouseEnterDelay={ENTER_DELAY} mouseLeaveDelay={LEAVE_DELAY} title="导出PDF">
|
||||||
|
<a id="nice-sidebar-pdf" className="nice-btn-pdf" onClick={this.handleClick}>
|
||||||
|
<SvgIcon name="pdf" className="nice-btn-pdf-icon" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Pdf;
|
10
src/component/Sidebar/Juejin.css
Normal file
10
src/component/Sidebar/Juejin.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.nice-btn-juejin {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-btn-juejin-icon {
|
||||||
|
padding: 5px 5px 5px 2px;
|
||||||
|
width: 30px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
48
src/component/Sidebar/Juejin.js
Normal file
48
src/component/Sidebar/Juejin.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {message, Tooltip} from "antd";
|
||||||
|
|
||||||
|
import {solveHtml, solveJuejinMath, solveJuejinCode, addJuejinSuffix, copySafari} from "../../utils/converter";
|
||||||
|
import {LAYOUT_ID, CODE_NUM, ENTER_DELAY, LEAVE_DELAY} from "../../utils/constant";
|
||||||
|
import SvgIcon from "../../icon";
|
||||||
|
import "./Juejin.css";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@inject("navbar")
|
||||||
|
@inject("imageHosting")
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class Juejin extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.html = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
copyJuejin = () => {
|
||||||
|
if (window.localStorage.getItem(CODE_NUM) === "0") {
|
||||||
|
message.warning("您当前使用的是微信代码主题,请切换其他代码主题后再试!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const layout = document.getElementById(LAYOUT_ID); // 保护现场
|
||||||
|
const html = layout.innerHTML;
|
||||||
|
solveJuejinMath();
|
||||||
|
addJuejinSuffix();
|
||||||
|
this.html = solveHtml();
|
||||||
|
this.html = solveJuejinCode(this.html);
|
||||||
|
copySafari(this.html);
|
||||||
|
message.success("已复制且添加 mdnice 排版后缀,感谢宣传,请到稀土掘金粘贴");
|
||||||
|
layout.innerHTML = html; // 恢复现场
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Tooltip placement="left" mouseEnterDelay={ENTER_DELAY} mouseLeaveDelay={LEAVE_DELAY} title="复制到稀土掘金">
|
||||||
|
<a id="nice-sidebar-juejin" className="nice-btn-juejin" onClick={this.copyJuejin}>
|
||||||
|
<SvgIcon name="juejin" className="nice-btn-juejin-icon" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Juejin;
|
10
src/component/Sidebar/Markdown.css
Normal file
10
src/component/Sidebar/Markdown.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.nice-btn-markdwon {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-btn-markdown-icon {
|
||||||
|
padding: 5px 5px 5px 2px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
35
src/component/Sidebar/Markdown.js
Normal file
35
src/component/Sidebar/Markdown.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {message, Tooltip} from "antd";
|
||||||
|
import SvgIcon from "../../icon";
|
||||||
|
|
||||||
|
import {ENTER_DELAY, LEAVE_DELAY, EXPORT_FILENAME_SUFFIX} from "../../utils/constant";
|
||||||
|
import {download, dateFormat} from "../../utils/helper";
|
||||||
|
|
||||||
|
import "./Markdown.css";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@observer
|
||||||
|
class ExportMarkdown extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const content = markdownEditor.getValue();
|
||||||
|
if ("download" in document.createElement("a")) {
|
||||||
|
download(content, dateFormat(new Date(), "yyyy-MM-dd") + EXPORT_FILENAME_SUFFIX);
|
||||||
|
} else {
|
||||||
|
message.warn("浏览器不支持");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Tooltip placement="left" mouseEnterDelay={ENTER_DELAY} mouseLeaveDelay={LEAVE_DELAY} title="导出Markdown">
|
||||||
|
<a id="nice-sidebar-markdown" className="nice-btn-markdown" onClick={this.handleClick}>
|
||||||
|
<SvgIcon name="markdown" className="nice-btn-markdown-icon" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExportMarkdown;
|
10
src/component/Sidebar/PreviewType.css
Normal file
10
src/component/Sidebar/PreviewType.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.nice-btn-previewtype {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-btn-previewtype-icon {
|
||||||
|
padding: 7px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
29
src/component/Sidebar/PreviewType.js
Normal file
29
src/component/Sidebar/PreviewType.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {Tooltip} from "antd";
|
||||||
|
|
||||||
|
import {ENTER_DELAY, LEAVE_DELAY} from "../../utils/constant";
|
||||||
|
import SvgIcon from "../../icon";
|
||||||
|
import "./PreviewType.css";
|
||||||
|
|
||||||
|
@inject("navbar")
|
||||||
|
@observer
|
||||||
|
class PreviewType extends Component {
|
||||||
|
handleClick = (key) => {
|
||||||
|
this.props.navbar.setPreviewType(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const targetType = this.props.navbar.previewType === "pc" ? "mobile" : "pc";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip placement="left" mouseEnterDelay={ENTER_DELAY} mouseLeaveDelay={LEAVE_DELAY} title="预览模式">
|
||||||
|
<a id="nice-sidebar-preview-type" className="nice-btn-previewtype" onClick={() => this.handleClick(targetType)}>
|
||||||
|
<SvgIcon name={targetType} className="nice-btn-previewtype-icon" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PreviewType;
|
10
src/component/Sidebar/Wechat.css
Normal file
10
src/component/Sidebar/Wechat.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.nice-btn-wechat {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-btn-wechat-icon {
|
||||||
|
padding: 6px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
42
src/component/Sidebar/Wechat.js
Normal file
42
src/component/Sidebar/Wechat.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {message, Tooltip} from "antd";
|
||||||
|
|
||||||
|
import {solveWeChatMath, solveHtml, copySafari} from "../../utils/converter";
|
||||||
|
import {LAYOUT_ID, ENTER_DELAY, LEAVE_DELAY} from "../../utils/constant";
|
||||||
|
import SvgIcon from "../../icon";
|
||||||
|
import "./Wechat.css";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@inject("navbar")
|
||||||
|
@inject("imageHosting")
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class Wechat extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.html = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
copyWechat = () => {
|
||||||
|
const layout = document.getElementById(LAYOUT_ID); // 保护现场
|
||||||
|
const html = layout.innerHTML;
|
||||||
|
solveWeChatMath();
|
||||||
|
this.html = solveHtml();
|
||||||
|
copySafari(this.html);
|
||||||
|
message.success("已复制,请到微信公众平台粘贴");
|
||||||
|
layout.innerHTML = html; // 恢复现场
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Tooltip placement="left" mouseEnterDelay={ENTER_DELAY} mouseLeaveDelay={LEAVE_DELAY} title="复制到公众号">
|
||||||
|
<a id="nice-sidebar-wechat" className="nice-btn-wechat" onClick={this.copyWechat}>
|
||||||
|
<SvgIcon name="wechat" className="nice-btn-wechat-icon" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Wechat;
|
10
src/component/Sidebar/Zhihu.css
Normal file
10
src/component/Sidebar/Zhihu.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.nice-btn-zhihu {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-btn-zhihu-icon {
|
||||||
|
padding: 5px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
46
src/component/Sidebar/Zhihu.js
Normal file
46
src/component/Sidebar/Zhihu.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {message, Tooltip} from "antd";
|
||||||
|
|
||||||
|
import {solveHtml, solveZhihuMath, copySafari} from "../../utils/converter";
|
||||||
|
import {LAYOUT_ID, CODE_NUM, ENTER_DELAY, LEAVE_DELAY} from "../../utils/constant";
|
||||||
|
import SvgIcon from "../../icon";
|
||||||
|
import "./Zhihu.css";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@inject("navbar")
|
||||||
|
@inject("imageHosting")
|
||||||
|
@inject("dialog")
|
||||||
|
@observer
|
||||||
|
class Zhihu extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.html = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
copyZhihu = () => {
|
||||||
|
if (window.localStorage.getItem(CODE_NUM) === "0") {
|
||||||
|
message.warning("您当前使用的是微信代码主题,请切换其他代码主题后再试!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const layout = document.getElementById(LAYOUT_ID); // 保护现场
|
||||||
|
const html = layout.innerHTML;
|
||||||
|
solveZhihuMath();
|
||||||
|
this.html = solveHtml();
|
||||||
|
copySafari(this.html);
|
||||||
|
message.success("已复制,请到知乎粘贴");
|
||||||
|
layout.innerHTML = html; // 恢复现场
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Tooltip placement="left" mouseEnterDelay={ENTER_DELAY} mouseLeaveDelay={LEAVE_DELAY} title="复制到知乎">
|
||||||
|
<a id="nice-sidebar-zhihu" className="nice-btn-zhihu" onClick={this.copyZhihu}>
|
||||||
|
<SvgIcon name="zhihu" className="nice-btn-zhihu-icon" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Zhihu;
|
10
src/component/Sidebar/pdf.css
Normal file
10
src/component/Sidebar/pdf.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.nice-btn-pdf {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nice-btn-pdf-icon {
|
||||||
|
padding: 6px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
42
src/component/Toolbar/Bold.js
Normal file
42
src/component/Toolbar/Bold.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {Tooltip} from "antd";
|
||||||
|
import {ENTER_DELAY, LEAVE_DELAY} from "../../utils/constant";
|
||||||
|
|
||||||
|
import SvgIcon from "../../icon";
|
||||||
|
import {bold} from "../../utils/editorKeyEvents";
|
||||||
|
import {hotKeys} from "../../utils/hotkey";
|
||||||
|
|
||||||
|
import "./common.css";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@observer
|
||||||
|
class Bold extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const selection = markdownEditor.getSelection();
|
||||||
|
bold(markdownEditor, selection);
|
||||||
|
|
||||||
|
// 上传后实时更新内容
|
||||||
|
const content = markdownEditor.getValue();
|
||||||
|
this.props.content.setContent(content);
|
||||||
|
markdownEditor.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
placement="bottom"
|
||||||
|
mouseEnterDelay={ENTER_DELAY}
|
||||||
|
mouseLeaveDelay={LEAVE_DELAY}
|
||||||
|
title={"快捷键:" + hotKeys.bold}
|
||||||
|
>
|
||||||
|
<a id="nice-sidebar-bold" className="nice-btn-tool" onClick={this.handleClick}>
|
||||||
|
<SvgIcon name="bold" className="nice-btn-tool-icon" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Bold;
|
42
src/component/Toolbar/Code.js
Normal file
42
src/component/Toolbar/Code.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {observer, inject} from "mobx-react";
|
||||||
|
import {ENTER_DELAY, LEAVE_DELAY} from "../../utils/constant";
|
||||||
|
import {Tooltip} from "antd";
|
||||||
|
|
||||||
|
import {code} from "../../utils/editorKeyEvents";
|
||||||
|
import {hotKeys} from "../../utils/hotkey";
|
||||||
|
import SvgIcon from "../../icon";
|
||||||
|
|
||||||
|
import "./common.css";
|
||||||
|
|
||||||
|
@inject("content")
|
||||||
|
@observer
|
||||||
|
class Code extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const {markdownEditor} = this.props.content;
|
||||||
|
const selection = markdownEditor.getSelection();
|
||||||
|
code(markdownEditor, selection);
|
||||||
|
|
||||||
|
// 上传后实时更新内容
|
||||||
|
const content = markdownEditor.getValue();
|
||||||
|
this.props.content.setContent(content);
|
||||||
|
markdownEditor.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
placement="bottom"
|
||||||
|
mouseEnterDelay={ENTER_DELAY}
|
||||||
|
mouseLeaveDelay={LEAVE_DELAY}
|
||||||
|
title={"快捷键:" + hotKeys.code}
|
||||||
|
>
|
||||||
|
<a id="nice-sidebar-code" className="nice-btn-tool" onClick={this.handleClick}>
|
||||||
|
<SvgIcon name="code" className="nice-btn-tool-icon" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Code;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user