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