21 Commits
Author SHA1 Message Date
Andy Dixon e4ce1293ae Merge pull request #21 from andydixon/v2-dev
V2 dev into master
2024-10-01 22:16:22 +01:00
Andy Dixon db3f33990e Add new page system implemented, and Select Page system implemented, Code tidied 2024-10-01 22:15:28 +01:00
Andy Dixon b60ba3e1da Initial code for New Page templating system 2024-10-01 20:35:16 +01:00
Andy Dixon 9e516dfb0f Merge pull request #18 from andydixon/v2-dev
Merge v2 into master
2024-09-18 22:05:57 +01:00
Andy Dixon bfbdbca433 Added README 2024-09-18 21:48:35 +01:00
Andy Dixon 34b4326c12 Add upload redirect 2024-09-18 21:41:08 +01:00
Andy Dixon a07febc5a6 Code Cleanup 2024-09-18 21:30:32 +01:00
Andy Dixon 7bf6d31693 Initial commit of v2 code 2024-09-18 20:30:21 +01:00
Andy Dixon 08def89313 Merge pull request #12 from andydixon/foag-merged
Backporting FOAG changes into Neo devstream
2024-09-10 16:21:44 +01:00
Andy Dixon ac13bf95a7 Backporting FOAG changes into Neo devstream 2024-09-10 16:20:14 +01:00
Andy Dixon 0875394e48 Merge pull request #9 from andydixon/dev
Dev
2024-08-01 21:16:58 +01:00
Andy Dixon c287e9de30 Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	cms/js/neocms.js
#	cms/settings/index.php
2024-08-01 21:15:41 +01:00
Andy Dixon 49e914bb6e Fixed issue #8 - Issue was related to being unable to support https, and also over https, the port number was being included. Added support for https and removal of the port number. Also fixed issue with calls going to legacy location instead of core. 2024-08-01 21:14:51 +01:00
Andy Dixon f9926ea2f0 Merge pull request #7 from andydixon/dev
Fixes Issue #5 and issue #6
2024-07-23 19:04:26 +01:00
Andy Dixon bd1567b788 Merge branch 'master' into dev 2024-07-23 19:03:59 +01:00
Andy Dixon 3e09519f5f Clean up CSS issues 2024-07-23 18:57:43 +01:00
Andy Dixon 2e0296eba1 Removed some simplified english, tag changes and cleared out small nuggets of dead code 2024-07-23 18:50:27 +01:00
Andy Dixon 581601bcb5 Merge pull request #4 from andydixon/issue-2
Issue #2 - Cleaned out unneeded functionality
2024-07-23 18:33:30 +01:00
Andy Dixon 854f7dc222 Issue #2 - Cleaned out unneeded functionality 2024-07-23 18:29:34 +01:00
Andy Dixon a20e5c1295 Update index.html
Fixed incorrect filename
2024-07-23 16:34:12 +01:00
Andy Dixon 5c41c928a6 Initial Commit 2024-07-23 16:21:39 +01:00
304 changed files with 7272 additions and 176287 deletions
-17
View File
@@ -1,17 +0,0 @@
# Site-specific credentials and deployment overrides.
/cms/config.local.php
# Runtime CMS state; retain only the web-access guards committed below.
/cms/data/*
!/cms/data/.htaccess
!/cms/data/index.php
/cms/logs/*
!/cms/logs/.htaccess
!/cms/logs/index.php
# Uploaded content belongs to the deployed site rather than source control.
/uploads/*
!/uploads/.htaccess
!/uploads/index.php
# Generated security-audit reports are retained deliberately and should remain visible to Git.
-674
View File
@@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://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.
NeoCMS - Flat File Content Management System
Copyright (C) 2024 Andy Dixon
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 <https://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:
Neo CMS (NeoCMS) Copyright (C) 2024 Andy Dixon
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
<https://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
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+14 -259
View File
@@ -1,270 +1,25 @@
# NeoCMS
## Version 3.0
## Version 2.0
Welcome to NeoCMS, the lightweight, database-free content management system. Built with PHP and JavaScript, NeoCMS is
designed to add controlled visual editing to an existing static website without changing how public pages are served.
Version 2.0 is a complete rewrite to ensure we support the latest jQuery and TinyMCE GPL version.
Version 3.0 adds filesystem-backed drafts, revision history, role-based permissions, scheduled publishing, page and media
management, SEO controls, shared content, navigation menus, responsive previews, accessibility checks, and an operational
dashboard. Public content remains ordinary HTML; the database server may continue its well-earned rest.
To install:
Further technical documentation:
* [Architecture](docs/ARCHITECTURE.md)
* [Development and testing](docs/DEVELOPMENT.md)
## Features
* No Database Required: NeoCMS works without a database, so you don’t have to worry about setup hassles or performance
slowdowns.
* Easy Content Editing: The TinyMCE visual editor updates marked content regions without requiring authors to edit HTML.
Adding `neo-dupe` allows an element and its children to be cloned or removed.
* Seamless Integration: Simply add the CMS to any existing static website. Just assign a special class to the elements
you want to make editable, and you’re all set.
* Consistent Layouts: NeoCMS supports page templates to keep your layouts neat and uniform across all pages.
* Drafts and Publishing: Editors can save drafts without changing the public page. Publishers can publish immediately or
schedule a publication.
* Revision History: Every changed or deleted page receives a timestamped filesystem snapshot that publishers can restore.
* Page Management: Search, create, duplicate, rename, and delete editable HTML pages, including pages in subdirectories.
* Media Library: Browse uploaded images, reuse them, maintain alternative text, see usage counts, and remove files.
* SEO Settings: Edit the page title, description, canonical URL, Open Graph image, and robots indexing setting.
* Shared Content: Mark content with `data-neo-shared="name"` and update matching regions across the site.
* Navigation Menus: Build ordered and nested navigation markup and insert it into an editable region.
* Responsive Preview: Preview the current page at desktop, tablet, and mobile widths.
* Accessibility Checks: Check common authoring problems such as missing alt text, empty links, heading jumps, missing labels,
and absent page metadata before publishing.
* Roles and Activity: Use editor, publisher, and administrator roles. The dashboard displays drafts, schedules, system
problems, and recent activity.
## Installation Guide
To install NeoCMS:
* Place the `cms/` directory in the document root of the static website.
* Place the `uploads/` directory in the same document root.
* Copy `cms/config.local.php.example` to `cms/config.local.php`, add password hashes, and assign roles.
* Update the elements you want to make editable by adding the configured editable class. It is `editable` by default.
* Confirm that PHP may write to the managed HTML files, `cms/data/`, `cms/logs/`, and `uploads/`.
* Visit `/cms/`, log in, and begin editing.
That’s it! You’re up and running.
## Configuration
Tracked defaults live in `cms/config.php`. Deployment credentials and overrides belong in the Git-ignored
`cms/config.local.php`, which must return an array. The supplied Apache rules deny direct access to both files.
| Setting | Type | Purpose |
| --- | --- | --- |
| `authentication` | array | Maps usernames to `password_hash()` values; plaintext credentials are rejected. |
| `roles` | array | Maps usernames to `editor`, `publisher`, or `administrator`. |
| `security` | array | Configures session expiry, login throttling, request limits, storage quotas, and audit-log retention. |
| `uploads` | array | Configures image size, dimension, file-count, and aggregate-storage limits. |
| `audit` | boolean | Enables daily activity logs under `cms/logs/`. |
| `skipWelcomePage` | boolean | Opens the public root instead of the built-in guide after login. |
| `showFullUrl` | boolean | Displays the current page path in the toolbar. |
| `editableClass` | string | Selects the single CSS class used to identify editable regions. |
`dataDirectory` is an internal override used by the integration suite and specialised deployments. Normal installations
should allow NeoCMS to use `cms/data/`.
### Editable Class
The class used to identify editable regions is configurable:
```php
'editableClass' => 'editable',
```
Set it to any single valid CSS class name without a leading dot, for example:
```php
'editableClass' => 'cms-content',
```
Your page markup must then use that class:
```html
<section class="cms-content">This region can be edited.</section>
```
Invalid configured values fall back to `editable`. The `neo-dupe` class remains fixed because it marks repeatable blocks,
not general editable content. When overriding `editableClass`, update the classes in `cms/templates/` as well so newly
created pages are immediately discoverable by the page picker.
### Users and Roles
Passwords must be generated with `password_hash()` and stored under `authentication` in `cms/config.local.php`. Assign the
same username a role under `roles`:
```php
'authentication' => [
'author' => '$2y$10$...',
'publisher' => '$2y$10$...',
],
'roles' => [
'author' => 'editor',
'publisher' => 'publisher',
],
```
* `editor`: edit content, save drafts, and upload media.
* `publisher`: editor permissions plus immediate and scheduled publishing and revision restoration.
* `administrator`: publisher permissions plus page, media, shared-content, and menu management.
Users without an explicit valid role receive `editor`, the least powerful role. Account removal, password-hash changes,
and role changes affect active sessions on their next request. Sessions expire after 30 minutes of inactivity and 12 hours
in total by default.
When HTTPS terminates at a trusted reverse proxy, set `'cookieSecure' => true` beneath `security` in the local override.
This keeps the session cookie marked Secure even when PHP receives the proxy's internal HTTP request.
Audit files rotate at 10 MiB and are retained for 90 days by default. Override `auditMaxFileBytes` and
`auditRetentionDays` beneath `security` where local retention obligations differ.
### Password Hash Example
Generate a hash on a trusted command line, then place only the resulting hash in `cms/config.local.php`:
```bash
php -r 'echo password_hash(trim(fgets(STDIN)), PASSWORD_DEFAULT), PHP_EOL;'
```
There is deliberately no enabled default account.
## Content Features
### Repeatable Blocks
Add `neo-dupe` alongside the configured editable class. The editor provides clone-before, clone-after, and delete controls:
```html
<article class="editable neo-dupe">Repeatable content</article>
```
### Shared Content
Use `data-neo-shared` to identify a global region:
```html
<footer class="editable" data-neo-shared="site-footer">Shared footer content</footer>
```
The Shared Content tool updates every HTML page containing the same key. Publishing a page also captures the current value
of its shared regions.
### Navigation Menus
Menu items use one line each in this format:
```text
Home | /
About | /about.html
Team | /team.html | About
```
The optional third value is the label of the parent item. After saving, the generated `<nav>` can be inserted into the
currently selected editable region.
### Drafts and Revisions
Drafts do not alter public HTML. When opening a page with a saved draft, NeoCMS offers to load it. Publishing creates a
revision of the previous public file first. Deleting a page also retains its last revision.
NeoCMS stores metadata, drafts, scheduled content, and revisions under `cms/data/`. Back up this directory together with
the website. The supplied `.htaccess` denies direct HTTP access on Apache; other web servers should deny `/cms/data/` and
`/cms/logs/` explicitly. Private directories and files are created with owner-only permissions where the operating system
permits it. Configurable quotas bound drafts, schedules, revisions, and uploaded media to reduce disk-exhaustion risk.
### Scheduled Publishing
Due schedules are processed whenever the CMS API is used. For publication without an active CMS user, run the CLI worker
once per minute with cron:
```cron
* * * * * /usr/bin/php /absolute/path/to/site/cms/publish_scheduled.php >/dev/null 2>&1
```
The server timezone controls PHP date handling; the browser converts the selected local time to an absolute timestamp.
## Administration Workflow
1. Open a page from **Pages**.
2. Select a region carrying the configured editable class and edit it in TinyMCE.
3. Use **Save Draft** to retain private work, or **Publish** to replace the public HTML after creating a revision.
4. Use **Schedule** when publication should occur later.
5. Check revisions, accessibility findings, SEO metadata, shared content, and menus through the relevant toolbar dialogues.
The Save button inside the content-editor modal only applies content to the in-browser preview. It does not write a public
file. This distinction is intentional and prevents an innocent wording change from making an unscheduled public debut.
## Page Markup Recommendations
For the best SEO and accessibility results, each page should include one `<h1>`, a non-empty `<title>`, a meta description,
alternative text on every image, descriptive link text, and labels associated with form fields. The checker highlights
common authoring issues but is not a complete accessibility audit.
* Drop in the CMS folder into a static website
* Drop in the uploads folder into the static website
* Edit credentials in /cms/init.php for any users that are required
* Update editable divs to contain the class 'editable'
* It _should_ then just work by visiting /cms/ and logging in
## Requirements
* PHP 8 running on Linux
* PHP DOM and fileinfo extensions
* PHP sessions enabled
* Write access to `cms/data/`, `cms/logs/`, `uploads/`, and managed HTML pages
* A modern browser with JavaScript enabled
* PHP 8 on Linux. None of that Windows Server crap
The administration shell loads jQuery 4.0.0 and jQuery UI 1.14.2 from the official jQuery CDN with Subresource Integrity.
TinyMCE 8.6.0 is bundled locally. Installations that must operate without internet access should self-host the two jQuery
assets and update both `cms/index.php` and the administration Content Security Policy.
## Known Bugs
## Web Server Security
* Templating (New Page) not yet implemented
* File (not images) uploads need to be implemented
* Not completely tested
Apache users should allow the supplied `.htaccess` rules. For Nginx, add equivalent rules such as:
```nginx
location ^~ /cms/data/ { deny all; }
location ^~ /cms/logs/ { deny all; }
location ~ ^/cms/(config(?:\.local)?\.php|config\.local\.php\.example|publish_scheduled\.php)$ { deny all; }
location ~ ^/uploads/.*\.(php[0-9]?|phtml|phar|cgi|pl|py|sh)$ { deny all; }
```
Also disable directory listings, prevent PHP source download through server misconfiguration, serve the CMS only over
HTTPS, and back up both public HTML and `cms/data/`. NeoCMS sends CSP, anti-framing, no-sniffing, referrer, permissions,
cross-origin isolation, no-store, and HTTPS HSTS headers where appropriate.
Set request-body limits in the web server and PHP as well as in NeoCMS. For the default upload and API limits, a 12 MiB
server limit leaves room for form overhead without letting oversized requests make themselves at home. For example:
```nginx
client_max_body_size 12m;
client_body_timeout 15s;
```
Match `post_max_size` and `upload_max_filesize` to your configured `security.maxRequestBytes` and `uploads.maxFileBytes`.
The page preview iframe is sandboxed with `allow-same-origin` but without `allow-scripts`. This preserves DOM editing while
preventing public page scripts from reaching the parent administration session.
## Testing
Run the dependency-free integration suite with:
```bash
php tests/run.php
```
It exercises authentication hardening, session revocation, login throttling, iframe sandboxing, path containment, content
limits, configurable editable regions, drafts, publication, revisions, page operations, shared content, menus, and
scheduled publishing in temporary directories. See [Development and testing](docs/DEVELOPMENT.md) for the complete check
list used by this repository.
## Licence
NeoCMS is open-source software licensed under the GNU General Public Licence v3.0 (GPLv3). You are free to use, modify,
and distribute this software under the terms of this licence.
For the complete terms, see [LICENSE](LICENSE).
## Found a Bug?
Report defects and feature requests through the project's GitHub issue tracker.
If any issues arise, raise it as a bug in the issues page on GitHub
-11
View File
@@ -1,11 +0,0 @@
# Prevent directory listings and direct access to configuration or CLI-only entry points.
Options -Indexes
<FilesMatch "^(config(?:\.local)?\.php|config\.local\.php\.example|publish_scheduled\.php)$">
Require all denied
</FilesMatch>
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "no-referrer"
</IfModule>
-21
View File
@@ -1,21 +0,0 @@
<?php
/**
* Copy this file to config.local.php and replace the example values.
* config.local.php is ignored by Git and denied by the supplied Apache rules.
*/
return [
'authentication' => [
'admin' => '$2y$10$replace-with-a-real-password-hash',
],
'roles' => [
'admin' => 'administrator',
],
// Set this to true when HTTPS is terminated by a trusted reverse proxy.
'security' => [
'cookieSecure' => true,
// Uncomment only where local audit-retention policy differs from the secure defaults.
// 'auditMaxFileBytes' => 10 * 1024 * 1024,
// 'auditRetentionDays' => 90,
],
];
-66
View File
@@ -1,66 +0,0 @@
<?php
/**
* NeoCMS site configuration.
*
* Keep credentials in the ignored config.local.php override rather than this tracked defaults file.
* No account is enabled by default; configure a password hash and role before exposing /cms/.
*/
$config = [
'authentication' => [
// 'admin' => '$2y$10$replace-with-a-real-password-hash',
],
// Grant each user the least powerful role they need: editor, publisher, or administrator.
'roles' => [
// 'admin' => 'administrator',
],
// Session expiry and login throttling. Set cookieSecure to true behind a TLS reverse proxy.
'security' => [
'idleTimeout' => 1800,
'absoluteTimeout' => 43200,
'cookieSecure' => null,
'loginWindowSeconds' => 900,
'loginLockoutSeconds' => 900,
'loginMaxAttempts' => 5,
'loginMaxAddressAttempts' => 20,
'maxContentBytes' => 5 * 1024 * 1024,
'maxRequestBytes' => 6 * 1024 * 1024,
'maxManagedPages' => 5000,
'maxScannedEntries' => 20000,
'maxDraftBytes' => 250 * 1024 * 1024,
'maxSchedules' => 100,
'maxScheduledBytes' => 250 * 1024 * 1024,
'maxRevisionsPerPage' => 50,
'maxRevisionsTotal' => 2000,
'maxRevisionBytes' => 500 * 1024 * 1024,
'auditMaxFileBytes' => 10 * 1024 * 1024,
'auditRetentionDays' => 90,
],
// Upload constraints limit both individual image complexity and total disk consumption.
'uploads' => [
'maxFileBytes' => 10 * 1024 * 1024,
'maxWidth' => 8192,
'maxHeight' => 8192,
'maxPixels' => 24 * 1024 * 1024,
'maxFiles' => 2000,
'maxTotalBytes' => 500 * 1024 * 1024,
],
// Record authentication and content-management activity in daily files under cms/logs/.
'audit' => true,
// Skip the built-in welcome page and open the public site immediately after login.
'skipWelcomePage' => false,
// Display the path of the page being edited in the administration toolbar.
'showFullUrl' => true,
// CSS class used to mark page regions as editable. Use a single class name without a leading dot.
'editableClass' => 'editable',
];
// Merge deployment credentials and overrides from the untracked local configuration, when present.
$localConfigPath = __DIR__ . '/config.local.php';
if (is_file($localConfigPath)) {
$localConfig = require $localConfigPath;
if (!is_array($localConfig)) {
throw new \RuntimeException('cms/config.local.php must return an array');
}
$config = array_replace_recursive($config, $localConfig);
}
+60 -16
View File
@@ -1,22 +1,66 @@
<?php
/**
* JSON API entry point for authenticated CMS operations.
*
* The controller performs authentication, authorisation, CSRF validation, and dispatch. Keeping
* this file thin makes it harder for an endpoint to wander off and invent its own security rules.
require_once "../neoCMSCore.php";
require_once "../init.php";
switch ($_REQUEST["action"]) {
case "save":
if (!empty($_POST['uri']) && !empty($_POST['content'])) {
$_SESSION['core']->saveContent($_POST['uri'], $_POST['content']);
echo 'Page Updated.';
} else {
echo 'Unable to save page';
}
break;
/**
* getTemplates AJAX call to get list of files
*/
case "getTemplates":
header('Content-type: application/json');
$files = array_diff(scandir("../templates/"), array('..', '.'));
$returnArray = [];
foreach ($files as $file) {
$returnArray[] = array("id" => $file, "name" => str_replace(array(".html", ".htm"), "", $file));
}
echo json_encode($returnArray);
break;
// Load the site-specific credentials, roles, and content settings.
require_once "../config.php";
/**
* newPage AJAX call to take a template file and create a new page
*/
case "newPage":
header('Content-type: application/json');
// Resolve NeoCMS namespaces to the matching class beneath cms/src/.
spl_autoload_register(function ($class) {
$classPath = str_replace('\\', DIRECTORY_SEPARATOR, $class);
require_once "../src/{$classPath}.php";
});
$filename = str_replace("..", "", $_POST['filename']);
$template = str_replace("..", "", $_POST['template']);
if (!str_ends_with($filename, ".htm") && !str_ends_with($filename, ".html")) {
$filename .= ".html";
}
use NeoCMS\CMSController;
// Some vhosts can supply a trailing slash on DOCUMENT_ROOT so tidy it up
$destination = str_replace("//", "/", $_SERVER['DOCUMENT_ROOT'] . "/" . $filename);
// Dispatch the current request and allow the controller to emit its JSON response.
$controller = new CMSController($config ?? []);
$controller->handleRequest();
if (!file_exists("../templates/" . $template)) die(json_encode(array("error" => "Template not found")));
if (file_exists($destination)) die(json_encode(array("error" => "Page or file already exists")));
file_put_contents($destination, file_get_contents("../templates/" . $template));
if (!file_exists($destination)) die(json_encode(array("error" => "Template copy failed")));
echo json_encode(array("ok" => "DONE_THE_NEEDFUL", "url" => str_replace("//", "/", "/" . $filename)));
break;
case "getPages":
header('Content-type: application/json');
$it = new RecursiveDirectoryIterator("../../");
$returnArray = [];
foreach (new RecursiveIteratorIterator($it) as $file) {
// Strip document root from URI, so relative
$file = str_replace($_SERVER["DOCUMENT_ROOT"], "", $file->getRealPath());
if (!str_starts_with($file, "/cms/") && (str_ends_with($file, ".html") || str_ends_with($file, ".htm")))
$returnArray[] = array("name" => $file, "url" => $file);
}
echo json_encode($returnArray);
break;
}
+69 -303
View File
@@ -1,176 +1,110 @@
/* Full-height application shell: the toolbar takes what it needs and the preview takes the rest. */
html,
body {
height: 100%;
margin: 0;
}
body {
min-height: 100vh;
}
.pageContainer {
display: flex;
flex-flow: column;
height: 100%;
min-height: 100vh;
width: 100%;
min-width: 0;
}
/* Dark, wrapping administration toolbar shared by wide and narrow screens. */
.pageContainer .controls {
flex: 0 0 auto;
min-height: 60px;
position: relative;
background-color: #1a1d20;
flex: 0 1 auto;
height: 70px;
background-color: #666666;
border-bottom: 1px solid #1a1a1a;
-webkit-box-shadow: 0 10px 13px -7px #000000, -1px -1px 15px 5px rgba(0, 0, 0, 0);
box-shadow: 0 10px 13px -7px #000000, -1px -1px 15px 5px rgba(0, 0, 0, 0);
display: flex;
align-items: center;
gap: 16px;
padding: 10px 16px;
box-sizing: border-box;
width: 100%;
min-width: 0;
flex-wrap: wrap;
-webkit-box-shadow: 0px 10px 13px -7px #000000, -1px -1px 15px 5px rgba(0, 0, 0, 0);
box-shadow: 0px 10px 13px -7px #000000, -1px -1px 15px 5px rgba(0, 0, 0, 0);
}
/* Same-origin preview iframe; JavaScript narrows its width for responsive previews. */
.pageContainer .frame {
flex: 1 1 auto;
display: block;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
border: 0;
}
/* Product mark remains a fixed size while neighbouring controls wrap around it. */
.controls .logo {
background-image: url("/cms/img/logo.png");
width: 60px;
height: 60px;
width: 128px;
height: 70px;
display: block;
flex: 0 0 60px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
position: absolute;
}
/* The two toolbar rows stay together and yield horizontal space when necessary. */
.controls .buttonContainer {
margin-left: auto;
flex: 0 1 auto;
min-width: 0;
width: 400px;
margin: 18px auto;
}
.toolbar-row {
display: flex;
gap: 7px;
justify-content: flex-end;
margin: 3px 0;
flex-wrap: wrap;
}
.publish-controls {
opacity: 0.94;
}
/* Shared toolbar button treatment, including keyboard focus and destructive emphasis. */
.headerButton {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 38px;
padding: 8px 14px;
font-family: -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif;
font-size: 14px;
font-weight: 600;
line-height: 1.2;
border-radius: 6px;
border: none;
white-space: nowrap;
cursor: pointer;
background: #6E6D70;
box-shadow: 0px 0.5px 1px rgba(0, 0, 0, 0.1), inset 0px 0.5px 0.5px rgba(255, 255, 255, 0.5), 0px 0px 0px 0.5px rgba(0, 0, 0, 0.12);
color: #DFDEDF;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.headerButton:focus {
box-shadow: inset 0px 0.8px 0px -0.25px rgba(255, 255, 255, 0.2), 0px 0.5px 1px rgba(0, 0, 0, 0.1), 0px 0px 0px 3.5px rgba(58, 108, 217, 0.5);
outline: 0;
}
.headerButton.dangerButton {
background-color: #6E4D50;
}
.headerButton:hover {
filter: brightness(1.08);
}
/* Current page path and unpublished-state label. */
.controls #urlbox {
flex: 1 1 320px;
min-width: 0;
text-align: center;
font-size: 1.1rem;
line-height: 1.3;
color: #dee2e6;
}
.controls #urlbox div {
display: inline-block;
margin-left: 10px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: bottom;
white-space: nowrap;
}
/* Compact account, role, logout, and operational-warning area. */
.controls .loggedInDetails {
flex: 0 1 auto;
color: #dee2e6;
text-align: right;
font-size: 0.9rem;
line-height: 1.4;
max-width: 260px;
position: absolute;
top: 25px;
right: 10px;
}
.link-button {
border: 0;
padding: 0;
color: #9ecbff;
background: transparent;
cursor: pointer;
}
/* Legacy list support retained for installations with locally customised toolbar markup. */
.controls .buttonContainer ul {
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.controls .buttonContainer li {
display: block;
display: inline;
margin-right: 10px;
}
/* New-page dialogue: template selection followed by the destination filename. */
.blueButton {
box-shadow: inset 0px 0px 15px 3px #23395e;
background: linear-gradient(to bottom, #2e466e 5%, #415989 100%);
background-color: #2e466e;
border-radius: 17px;
border: 1px solid #1f2f47;
display: inline-block;
cursor: pointer;
color: #ffffff;
font-family: Arial;
font-size: 15px;
padding: 6px 13px;
text-decoration: none;
text-shadow: 0px 1px 0px #263666;
}
.blueButton:hover {
background: linear-gradient(to bottom, #415989 5%, #2e466e 100%);
background-color: #415989;
}
.greenButton {
box-shadow: inset 0px 0px 15px 3px #3e7327;
background: linear-gradient(to bottom, #77b55a 5%, #72b352 100%);
background-color: #77b55a;
border-radius: 17px;
border: 1px solid #4b8f29;
display: inline-block;
cursor: pointer;
color: #ffffff;
font-family: Arial;
font-size: 15px;
padding: 6px 13px;
text-decoration: none;
text-shadow: 0px 1px 0px #5b8a3c;
}
.greenButton:hover {
background: linear-gradient(to bottom, #72b352 5%, #77b55a 100%);
background-color: #72b352;
}
.greenButton:active {
position: relative;
top: 1px;
}
/**
New Page Modal
*/
#newPageDialog {
display: none;
@@ -202,7 +136,9 @@ body {
display: block;
}
/* Page-picker dialogue and searchable action rows. */
/**
File List Modal
*/
#fileListDialog {
display: none;
@@ -228,178 +164,8 @@ body {
margin-bottom: 5px;
}
#pageSearch {
width: 100%;
padding: 9px;
margin-bottom: 12px;
box-sizing: border-box;
}
#fileList li {
display: flex;
gap: 6px;
align-items: center;
}
#fileList .page-open {
flex: 1;
text-align: left;
}
.cms-dialog label {
display: block;
margin: 12px 0;
font-weight: 600;
}
/* Consistent form controls across SEO, scheduling, shared-content, and menu dialogues. */
.cms-dialog input[type="text"],
.cms-dialog input[type="url"],
.cms-dialog input[type="datetime-local"],
.cms-dialog textarea {
display: block;
width: 100%;
margin-top: 5px;
padding: 8px;
box-sizing: border-box;
font-weight: normal;
}
/* Content-editor actions sit beneath TinyMCE now that no Bootstrap modal is required. */
.editor-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 12px;
}
/* Tool launcher expands to available width without requiring a row of tiny buttons. */
.tool-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.tool-grid button,
.cms-dialog button,
#fileList button {
padding: 7px 10px;
cursor: pointer;
}
/* Media cards keep previews useful while accommodating long generated filenames. */
.media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 14px;
max-height: 65vh;
overflow: auto;
}
.media-card {
display: flex;
flex-direction: column;
gap: 7px;
padding: 10px;
border: 1px solid #ccc;
overflow: hidden;
}
.media-card img {
width: 100%;
height: 130px;
object-fit: contain;
background: #eee;
cursor: pointer;
}
/* Reusable rows serve revisions, schedules, activity, shared blocks, and menus. */
.list-row {
display: block;
width: 100%;
margin: 5px 0;
padding: 8px;
border-bottom: 1px solid #ddd;
text-align: left;
box-sizing: border-box;
}
/* Findings use a restrained warning treatment; no flashing sirens are required. */
.issue {
margin: 7px 0;
padding: 9px;
border-left: 4px solid #b52b3a;
background: #fff0f1;
}
/* Semantic status colours shared by checks and destructive actions. */
.success-text {
color: #18733c;
font-weight: 600;
}
.danger-text {
color: #a51d2d;
}
#fileList li:hover {
background-color: #e0e0e0;
}
.redText {
color: #ff4d4d;
}
/* At medium widths the page path receives its own row. */
@media (max-width: 1100px) {
.pageContainer .controls {
gap: 12px;
}
.controls #urlbox {
order: 3;
flex-basis: 100%;
text-align: left;
}
.controls .buttonContainer {
margin-left: 0;
}
}
/* On small screens controls stack into touch-friendly full-width rows. */
@media (max-width: 760px) {
.pageContainer .controls {
align-items: flex-start;
padding: 12px;
}
.controls .logo {
width: 48px;
height: 48px;
flex-basis: 48px;
}
.controls .buttonContainer,
.controls .loggedInDetails,
.controls #urlbox {
flex-basis: 100%;
max-width: none;
text-align: left;
}
.controls .buttonContainer ul {
gap: 8px;
}
.controls .buttonContainer li,
.controls .buttonContainer .headerButton {
width: 100%;
}
.controls #urlbox div {
display: block;
margin-left: 0;
margin-top: 4px;
}
}
-81
View File
@@ -1,81 +0,0 @@
/* Standalone login palette kept deliberately separate from the administration application. */
body {
font-family: Arial, sans-serif;
background-color: #111519;
}
/* Centred credential card with enough contrast to survive a late-night publishing session. */
.login-container {
width: 350px;
margin: 100px auto;
padding: 40px;
background-color: #212529;
border-radius: 8px;
box-shadow: 0 4px 10px rgb(0, 0, 0);
}
/* Form typography and labels follow the same light-on-dark visual hierarchy. */
.login-container h2 {
text-align: center;
margin-bottom: 30px;
color: #dee2e6;
}
.login-container label {
display: block;
margin-bottom: 8px;
color: #dee2e6;
}
/* Inputs use border-box so their declared width includes padding and borders. */
.login-container input[type="text"],
.login-container input[type="password"] {
width: 100%;
padding: 12px;
margin-bottom: 20px;
border: 1px solid #1a1a1a;
border-radius: 4px;
box-sizing: border-box;
background: #2b3035;
color: #dee2e6;
}
/* The single primary action spans the card and receives a clear hover state. */
.login-container button {
width: 100%;
padding: 12px;
background-color: #0066cc;
border: none;
border-radius: 4px;
color: #ffffff;
font-size: 16px;
cursor: pointer;
}
.login-container button:hover {
background-color: #005bb5;
}
/* Authentication failures remain visible without exposing sensitive detail. */
.error-message {
color: #ff4d4d;
margin-bottom: 20px;
text-align: center;
}
/* Brand artwork centres independently of its source image dimensions. */
.logo {
display: block;
margin: auto;
}
/* Low-contrast project credit: present, but not competing with the login form. */
.shamelessPlug {
margin: 12px 0 -30px 0;
padding: 0;
display: block;
text-align: center;
color: #111519;
font-size: 0.8em;
}
+65
View File
@@ -0,0 +1,65 @@
/* General Styles */
body {
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f9f9f9;
color: #333;
}
/* Header Styles */
header {
background-color: #1b6ac9;
padding: 20px;
text-align: center;
}
.logo {
height: 80px;
}
/* Main Content Styles */
main {
text-align: center;
padding: 60px 20px;
}
h1 {
font-size: 2.5em;
color: #1b6ac9;
margin-bottom: 20px;
}
p {
font-size: 1.1em;
line-height: 1.6;
max-width: 700px;
margin: 10px auto;
}
/* Button Styles */
.button {
display: inline-block;
margin-top: 30px;
padding: 15px 30px;
background-color: #1b6ac9;
color: #fff;
text-decoration: none;
font-size: 1em;
border-radius: 5px;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #155a9c;
}
/* Footer Styles */
footer {
background-color: #333;
color: #fff;
padding: 15px 0;
text-align: center;
position: fixed;
width: 100%;
bottom: 0;
}
-2
View File
@@ -1,2 +0,0 @@
# NeoCMS metadata may contain drafts and revision history; never serve it directly.
Require all denied
-3
View File
@@ -1,3 +0,0 @@
<?php
// Metadata is private even when a web server ignores the accompanying .htaccess file.
http_response_code(404);
+22 -196
View File
@@ -1,223 +1,49 @@
<?php
/**
* Authenticated TinyMCE image-upload endpoint.
*
* Files are checked by byte size, detected MIME type, and image decoding before receiving a random
* server filename. Trusting only the browser-supplied extension would be admirably optimistic.
*/
// Allowed MIME types
$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
// Load credentials, role assignments, and shared application settings.
require_once "config.php";
// Resolve project classes without requiring Composer for this deliberately lightweight CMS.
spl_autoload_register(function ($class) {
$classPath = str_replace('\\', DIRECTORY_SEPARATOR, $class);
require_once "./src/{$classPath}.php";
});
use NeoCMS\Authentication;
use NeoCMS\FileStore;
use NeoCMS\SecurityHeaders;
SecurityHeaders::json(isset($config['security']['cookieSecure']) ? (bool) $config['security']['cookieSecure'] : null);
// Authentication is required before any upload details are processed.
$authentication = new Authentication($config['authentication'] ?? [], $config['roles'] ?? [], $config['security'] ?? []);
if (!$authentication->isLoggedIn()) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorised']);
exit;
}
// Editors may upload; unauthorised roles receive a clear forbidden response.
if (!$authentication->can('upload')) {
http_response_code(403);
echo json_encode(['error' => 'Your role cannot upload files']);
exit;
}
// Uploads alter server state and therefore accept POST only.
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
header('Allow: POST');
echo json_encode(['error' => 'Invalid request method']);
exit;
}
// Accept the token as form data or a conventional request header.
$csrfToken = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;
if (!$authentication->isValidCsrfToken($csrfToken)) {
http_response_code(403);
echo json_encode(['error' => 'Invalid CSRF token']);
exit;
}
// Map explicitly supported image MIME types to controlled filename extensions.
$allowed_types = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
];
// Apply configured byte, dimension, pixel-count, file-count, and total-storage limits.
$uploadConfig = is_array($config['uploads'] ?? null) ? $config['uploads'] : [];
$max_file_size = max(1024, (int) ($uploadConfig['maxFileBytes'] ?? 10 * 1024 * 1024));
$maxWidth = max(1, (int) ($uploadConfig['maxWidth'] ?? 8192));
$maxHeight = max(1, (int) ($uploadConfig['maxHeight'] ?? 8192));
$maxPixels = max(1, (int) ($uploadConfig['maxPixels'] ?? 24 * 1024 * 1024));
$maxFiles = max(1, (int) ($uploadConfig['maxFiles'] ?? 2000));
$maxTotalBytes = max($max_file_size, (int) ($uploadConfig['maxTotalBytes'] ?? 500 * 1024 * 1024));
if ((int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > $max_file_size + 1024 * 1024) {
http_response_code(413);
echo json_encode(['error' => 'Upload request is too large.']);
exit;
}
// Continue only when PHP reports a complete upload.
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
// Check if a file has been uploaded
if (isset($_FILES['file'])) {
$file = $_FILES['file'];
// Enforce the application limit even if php.ini permits larger files.
if ($file['size'] > $max_file_size) {
http_response_code(400);
echo json_encode(['error' => 'File size exceeds the maximum limit.']);
exit;
}
// Inspect file bytes rather than trusting the user-controlled original filename.
// Validate the file type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!array_key_exists($mime_type, $allowed_types)) {
if (!in_array($mime_type, $allowed_types)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid file type.']);
exit;
}
// Confirm that an image decoder recognises the payload as an actual image.
$imageInfo = getimagesize($file['tmp_name']);
if ($imageInfo === false) {
http_response_code(400);
echo json_encode(['error' => 'Uploaded file is not a valid image.']);
exit;
}
[$width, $height] = $imageInfo;
if ($width > $maxWidth || $height > $maxHeight || $width * $height > $maxPixels) {
http_response_code(400);
echo json_encode(['error' => 'Image dimensions exceed the configured limit.']);
exit;
// Set the upload directory
$upload_dir = '../uploads/';
// Ensure the upload directory exists
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
// Random names prevent collisions and avoid publishing the original local filename.
$extension = $allowed_types[$mime_type];
$filename = bin2hex(random_bytes(16)) . '.' . $extension;
// Generate a unique name for the image
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = uniqid() . '.' . $extension;
// Create the public upload directory on a fresh installation.
$uploadRoot = __DIR__ . '/../uploads';
if (!is_dir($uploadRoot) && !mkdir($uploadRoot, 0755, true)) {
http_response_code(500);
echo json_encode(['error' => 'Failed to create upload directory.']);
exit;
}
$upload_dir = realpath($uploadRoot);
if ($upload_dir === false) {
http_response_code(500);
echo json_encode(['error' => 'Upload directory is invalid.']);
exit;
}
$upload_dir .= DIRECTORY_SEPARATOR;
@chmod($upload_dir, 0755);
// Build the final path from the canonical upload directory and generated safe filename.
// Set the target file path
$target_file = $upload_dir . $filename;
// Serialise quota checks and moves so concurrent uploads cannot race past the storage ceiling.
$store = new FileStore((string) ($config['dataDirectory'] ?? (__DIR__ . '/data')));
$lockPath = $store->directory('locks') . 'uploads.lock';
$uploadLock = fopen($lockPath, 'c+');
if ($uploadLock === false || !flock($uploadLock, LOCK_EX)) {
if (is_resource($uploadLock)) {
fclose($uploadLock);
}
http_response_code(503);
echo json_encode(['error' => 'Upload service is temporarily unavailable.']);
exit;
}
@chmod($lockPath, 0600);
$existingFiles = array_values(array_filter(glob($upload_dir . '*') ?: [], 'is_file'));
$existingBytes = array_sum(array_map(static fn(string $path): int => (int) filesize($path), $existingFiles));
if (count($existingFiles) >= $maxFiles || $existingBytes + (int) $file['size'] > $maxTotalBytes) {
flock($uploadLock, LOCK_UN);
fclose($uploadLock);
http_response_code(507);
echo json_encode(['error' => 'Upload storage quota has been reached.']);
exit;
}
// move_uploaded_file() additionally confirms that PHP received the source as an upload.
// Move the uploaded file to the target directory
if (move_uploaded_file($file['tmp_name'], $target_file)) {
// Public images need read access but no executable or world-writable permissions.
chmod($target_file, 0644);
// Seed media metadata from the original basename; editors can improve the alt text later.
$location = '/uploads/' . $filename;
$metadata = $store->read('media');
$originalBase = pathinfo((string) $file['name'], PATHINFO_FILENAME);
$altText = substr(preg_replace('/[\x00-\x1F\x7F]/u', '', $originalBase) ?? '', 0, 200);
$metadata[$filename] = ['alt' => $altText];
try {
$store->write('media', $metadata);
} catch (\Throwable $exception) {
@unlink($target_file);
flock($uploadLock, LOCK_UN);
fclose($uploadLock);
http_response_code(500);
echo json_encode(['error' => 'Unable to record uploaded media.']);
exit;
}
flock($uploadLock, LOCK_UN);
fclose($uploadLock);
// Return a JSON response with the image location
$location = $target_file;
echo json_encode(['location' => $location]);
} else {
flock($uploadLock, LOCK_UN);
fclose($uploadLock);
// A failed move is a server error because validation has already succeeded.
// Return an error response
http_response_code(500);
echo json_encode(['error' => 'Failed to move uploaded file.']);
}
} else {
// Translate PHP's numeric upload failures into messages an editor can act upon.
$error_message = 'No file uploaded.';
if (isset($_FILES['file']['error'])) {
switch ($_FILES['file']['error']) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$error_message = 'File size exceeds the maximum limit.';
break;
case UPLOAD_ERR_PARTIAL:
$error_message = 'The uploaded file was only partially uploaded.';
break;
case UPLOAD_ERR_NO_FILE:
$error_message = 'No file was uploaded.';
break;
case UPLOAD_ERR_NO_TMP_DIR:
$error_message = 'Missing a temporary folder.';
break;
case UPLOAD_ERR_CANT_WRITE:
$error_message = 'Failed to write file to disk.';
break;
case UPLOAD_ERR_EXTENSION:
$error_message = 'A PHP extension stopped the file upload.';
break;
default:
$error_message = 'Unknown upload error.';
break;
}
}
// No file was uploaded
http_response_code(400);
echo json_encode(['error' => $error_message]);
echo json_encode(['error' => 'No file uploaded.']);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

+46 -143
View File
@@ -1,104 +1,73 @@
<?php
/** Server-rendered shell for the authenticated NeoCMS administration application. */
// Load configuration before initialising sessions and role-aware controls.
require_once "config.php";
// Resolve project classes without requiring Composer.
spl_autoload_register(function ($class) {
$classPath = str_replace('\\', DIRECTORY_SEPARATOR, $class);
require_once "./src/{$classPath}.php";
});
use NeoCMS\Authentication;
use NeoCMS\SecurityHeaders;
// Prepare the session values required by both the rendered shell and JavaScript client.
$authentication = new Authentication($config['authentication'] ?? [], $config['roles'] ?? [], $config['security'] ?? []);
$csrfToken = $authentication->getCsrfToken();
// Only a single valid CSS class is allowed; malformed values fall back safely.
$editableClass = $config['editableClass'] ?? 'editable';
if (!is_string($editableClass) || !preg_match('/^[a-zA-Z_][a-zA-Z0-9_-]*$/', $editableClass)) {
$editableClass = 'editable';
}
// Keep the administration shell private by redirecting anonymous requests to login.
if (!$authentication->isLoggedIn()) {
header("Location: /cms/login/");
exit;
}
SecurityHeaders::html(true, isset($config['security']['cookieSecure']) ? (bool) $config['security']['cookieSecure'] : null);
require_once "neoCMSCore.php";
require_once "init.php";
?>
<!DOCTYPE html>
<html lang="en-gb">
<head>
<title>NeoCMS</title>
<meta name="csrf-token" content="<?php echo htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
<meta name="neo-editable-class" content="<?php echo htmlspecialchars($editableClass, ENT_QUOTES, 'UTF-8'); ?>">
<script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha384-fgGyf7Mo7DURSOMnOy7ed+dkq5Job205Gnzu6QIg0BOHKaqt4D76Dt8VlDCzcMHV" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/ui/1.14.2/jquery-ui.min.js" integrity="sha384-tBcEcHGtNy7/Mx08+YxuvQ6v6s0N2jgehtFiT+bLtGwTj/txXtB/L5GqXfggm5sS" crossorigin="anonymous"></script>
<!-- Include jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://code.jquery.com/ui/1.14.0/jquery-ui.min.js" crossorigin="anonymous"></script>
<!-- Include TinyMCE -->
<script src="/cms/tinymce/tinymce.min.js"></script>
<link rel="stylesheet" href="https://code.jquery.com/ui/1.14.2/themes/base/jquery-ui.css" integrity="sha384-pUvA/6DQjteMxpaV6uGxZ1QuYrFLJgrLMvBWf06VcJIg6ky/Y5m3UZJlrv11V1I+" crossorigin="anonymous">
<!-- Include Bootstrap for modal -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<!-- Include JqueryUI CSS -->
<link rel="stylesheet" href="https://code.jquery.com/ui/1.14.0/themes/base/jquery-ui.css">
<!-- Include editor styles -->
<link rel="stylesheet" href="/cms/css/editor.css">
<!-- Include Bootstrap JS -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body data-role="<?php echo htmlspecialchars($authentication->getRole(), ENT_QUOTES, 'UTF-8'); ?>">
<!-- Status messages sit outside the flex layout so they may overlay the full viewport. -->
<div id="message-bar" style="display:none;"></div>
<body>
<div class="pageContainer">
<div class="controls">
<div class="logo"></div>
<?php if ($config['showFullUrl']) {?>
<div id="urlbox"></div>
<?php } ?>
<!-- Primary content tools and publication controls are grouped into two wrapping rows. -->
<div class="buttonContainer">
<div class="toolbar-row">
<button class="headerButton" id="dashboardButton">Dashboard</button>
<button class="headerButton manage-only" id="newPage">New Page</button>
<button class="headerButton" id="selectPage">Pages</button>
<button class="headerButton" id="mediaButton">Media</button>
<button class="headerButton" id="seoButton">SEO</button>
<button class="headerButton" id="moreButton">Tools</button>
</div>
<div class="toolbar-row publish-controls">
<button class="headerButton" id="saveDraft">Save Draft</button>
<button class="headerButton publish-only" id="scheduleButton">Schedule</button>
<button class="headerButton dangerButton publish-only" id="savePage">Publish</button>
<button class="headerButton viewport-button" data-width="100%">Desktop</button>
<button class="headerButton viewport-button" data-width="768px">Tablet</button>
<button class="headerButton viewport-button" data-width="390px">Mobile</button>
</div>
<ul>
<li><a href="#" id="selectPage" class="blueButton">Select Page</a></li>
<li><a href="#" id="newPage" class="blueButton">New Page</a></li>
<li><a href="#" id="savePage" class="greenButton">Save Changes</a></li>
</ul>
</div>
<div class="loggedInDetails">
<?php echo htmlspecialchars($authentication->getLoggedInUser(), ENT_QUOTES, 'UTF-8'); ?>
(<?php echo htmlspecialchars($authentication->getRole(), ENT_QUOTES, 'UTF-8'); ?>)<br/>
<button id="logoutButton" class="link-button">Log out</button><br/>
<?php
if (!is_writable("logs/")) {
echo "<span class='redText'>Make /cms/logs/ writable.</span>";
}
?>
Logged in as: <?php echo $_SESSION["core"]->getLoggedinUser(); ?>
</div>
</div>
<iframe id="frameContainer" src="<?php echo $config['skipWelcomePage'] ? "/" : "welcome.html"; ?>"
class="frame" sandbox="allow-same-origin"></iframe>
<iframe id="frameContainer" src="welcome.html" class="frame"></iframe>
</div>
<!-- TinyMCE content editor. Saving here changes the preview, not the public page. -->
<div id="editModal" class="cms-dialog" title="Edit Content">
<!-- Modal -->
<div id="editModal" class="modal fade" role="dialog">
<div class="modal-dialog modal-lg">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Edit Content</h4>
</div>
<div class="modal-body">
<textarea id="editor"></textarea>
<div class="editor-actions">
<button id="saveBtn" type="button">Save</button>
<button id="closeEditorBtn" type="button">Close</button>
</div>
<div class="modal-footer">
<button id="saveBtn" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Template and target path controls for administrator page creation. -->
<!--New Page modal -->
<div id="newPageDialog" title="Create a New Page">
<div class="newpage-content">
<form id="newPageForm">
<div id="radioList">
<!-- JavaScript populates available filesystem templates here. -->
<!-- Radio buttons will be populated here -->
</div>
<label for="filename">New Filename:</label>
<input type="text" id="filename" name="filename" placeholder="Enter new filename" required>
@@ -108,81 +77,15 @@ SecurityHeaders::html(true, isset($config['security']['cookieSecure']) ? (bool)
</div>
</div>
<!-- Searchable page picker with role-dependent management actions. -->
<!-- Page list modal -->
<div id="fileListDialog" title="Select an Existing Page">
<div class="filelist-content">
<input id="pageSearch" type="search" placeholder="Search pages">
<ul id="fileList">
<!-- JavaScript populates discovered editable pages here. -->
<!-- File names will be populated here -->
</ul>
</div>
</div>
<!-- Operational overview: pending work, schedules, activity, and filesystem warnings. -->
<div id="dashboardDialog" class="cms-dialog" title="Dashboard"><div id="dashboardContent"></div></div>
<!-- Secondary authoring and site-wide management tools. -->
<div id="toolsDialog" class="cms-dialog" title="Tools">
<div class="tool-grid">
<button id="revisionsButton">Revision history</button>
<button id="accessibilityButton">Accessibility check</button>
<button id="sharedButton" class="manage-only">Shared content</button>
<button id="menusButton" class="manage-only">Navigation menus</button>
</div>
</div>
<!-- Reusable uploaded-image browser and metadata editor. -->
<div id="mediaDialog" class="cms-dialog" title="Media Library">
<p>Select an image to insert it into the open editable region.</p>
<div id="mediaList" class="media-grid"></div>
</div>
<!-- Page history and publisher-only restoration controls. -->
<div id="revisionsDialog" class="cms-dialog" title="Revision History"><div id="revisionsList"></div></div>
<!-- In-document title, search metadata, canonical URL, and robots settings. -->
<div id="seoDialog" class="cms-dialog" title="SEO and Page Settings">
<form id="seoForm">
<label>Page title<input id="seoTitle" type="text" required></label>
<label>Meta description<textarea id="seoDescription"></textarea></label>
<label>Canonical URL<input id="seoCanonical" type="url"></label>
<label>Open Graph image<input id="seoImage" type="text" placeholder="/uploads/image.jpg"></label>
<label><input id="seoNoIndex" type="checkbox"> Ask search engines not to index this page</label>
<button type="submit">Apply settings</button>
</form>
</div>
<!-- Future publication time is entered locally and submitted as an absolute timestamp. -->
<div id="scheduleDialog" class="cms-dialog" title="Schedule Publication">
<form id="scheduleForm">
<label>Publish date and time<input id="publishAt" type="datetime-local" required></label>
<button type="submit">Schedule</button>
</form>
</div>
<!-- Results from the browser-side authoring accessibility checks. -->
<div id="accessibilityDialog" class="cms-dialog" title="Accessibility Check"><div id="accessibilityResults"></div></div>
<!-- Site-wide shared-region registry and propagation form. -->
<div id="sharedDialog" class="cms-dialog" title="Shared Content">
<form id="sharedForm">
<label>Block name<input id="sharedKey" type="text" pattern="[A-Za-z0-9_-]+" required></label>
<label>HTML content<textarea id="sharedContent" rows="8" required></textarea></label>
<button type="submit">Save and update every page</button>
</form>
<div id="sharedList"></div>
</div>
<!-- Line-oriented menu editor supporting an optional parent label for nesting. -->
<div id="menusDialog" class="cms-dialog" title="Navigation Menus">
<form id="menuForm">
<label>Menu name<input id="menuName" type="text" pattern="[A-Za-z0-9_-]+" required></label>
<label>One item per line: Label | URL | Optional parent label<textarea id="menuItems" rows="9" placeholder="Home | /&#10;About | /about.html&#10;Team | /team.html | About" required></textarea></label>
<button type="submit">Save menu</button>
</form>
<div id="menuList"></div>
</div>
<script src="/cms/js/cms.js"></script>
</body>
+56
View File
@@ -0,0 +1,56 @@
<?php
/*
* Authentication stores the usernames and passwords for people to be able to use the CMS.
* This will be used as well to write to the audit log once it has been implemented in code.
*/
$authentication = [
"admin" => "password",
"editor" => "password",
];
/**
* DO NOT CHANGE ANYTHING BENEATH THIS LINE - IT WILL BUGGER STUFF UP
*/
session_start();
require_once "neoCMSCore.php";
if (empty($_SESSION["core"])) {
$_SESSION["core"] = new neoCMSCore();
$_SESSION["core"]->users = $authentication;
}
if (!$_SESSION["core"]->isLoggedIn() && stripos($_SERVER['REQUEST_URI'], "/login") === FALSE) {
header('Location: /cms/login');
die("Redirecting to login..");
}
// str_starts_with compatibility for php 7
if (!function_exists('str_starts_with')) {
function str_starts_with($haystack, $needle)
{
//str_starts_with(string $haystack, string $needle): bool
$strlen_needle = mb_strlen($needle);
if (mb_substr($haystack, 0, $strlen_needle) == $needle) {
return true;
}
return false;
}
}
//str_ends_with compatibility for php 7
if (!function_exists('str_ends_with')) {
function str_ends_with($haystack, $needle)
{
//str_starts_with(string $haystack, string $needle): bool
$strlen_needle = mb_strlen($needle);
if (mb_substr($haystack, -$strlen_needle, $strlen_needle) == $needle) {
return true;
}
return false;
}
}
$_SESSION["core"]->version = '2.00';
+155 -610
View File
@@ -1,646 +1,191 @@
/**
* NeoCMS administration client.
*
* The public page is loaded into a same-origin iframe, edited in memory, and only written to disk
* through authenticated API calls. Keeping that distinction explicit prevents a modal's Save
* button from accidentally becoming a rather enthusiastic Publish button.
*/
(function ($) {
'use strict';
$(document).ready(function () {
// References to the active iframe document and currently selected editable element.
let iframeDoc = null;
let currentElement = null;
var currentDiv;
var iframeDoc;
// Dirty state protects unpublished browser changes from accidental navigation.
let dirty = false;
$('#frameContainer').on('load', function () {
// Permissions are replaced by the authoritative dashboard response after initialisation.
let permissions = {draft: true, publish: false, schedule: false, manage: false};
// Remember offered drafts so an iframe rewrite does not prompt for the same draft twice.
const loadedDrafts = new Set();
// Server-rendered values keep selectors and mutating requests aligned with configuration.
const csrfToken = $('meta[name="csrf-token"]').attr('content') || '';
const editableClass = $('meta[name="neo-editable-class"]').attr('content') || 'editable';
const editableSelector = '.' + editableClass;
// Initialise controls after the administration document is ready.
$(function () {
initialiseDialogs();
bindToolbar();
loadDashboard(false);
$('#frameContainer').on('load', initialiseFrame);
// Browsers display their own standard warning text for beforeunload events.
window.addEventListener('beforeunload', function (event) {
if (dirty) {
event.preventDefault();
event.returnValue = '';
}
});
});
/**
* Call one controller action and convert failed JSON responses into ordinary Error objects.
* POST requests automatically include the session CSRF token.
*/
function api(action, data, method) {
const payload = Object.assign({}, data || {}, {action: action});
if ((method || 'GET') === 'POST') {
payload.csrf_token = csrfToken;
}
return $.ajax({url: '/cms/controller/', method: method || 'GET', data: payload, dataType: 'json'})
.catch(function (xhr) {
const response = xhr.responseJSON || {};
throw new Error(response.error || 'The CMS request failed');
});
}
/** Configure all jQuery UI dialogues once, leaving individual workflows to populate them. */
function initialiseDialogs() {
$('.cms-dialog, #newPageDialog, #fileListDialog').hide();
$('.cms-dialog, #newPageDialog, #fileListDialog').not('#editModal').each(function () {
$(this).dialog({autoOpen: false, modal: true, width: Math.min(760, window.innerWidth - 30)});
});
$('#editModal').dialog({
autoOpen: false,
modal: true,
width: Math.min(900, window.innerWidth - 30),
open: initialiseEditor,
close: destroyEditor
});
}
/** Connect toolbar controls and forms to their workflow handlers. */
function bindToolbar() {
$('#dashboardButton').on('click', function () { loadDashboard(true); });
$('#newPage').on('click', openNewPage);
$('#selectPage').on('click', openPages);
$('#saveDraft').on('click', saveDraft);
$('#savePage').on('click', publishPage);
$('#scheduleButton').on('click', openSchedule);
$('#mediaButton').on('click', openMedia);
$('#seoButton').on('click', openSeo);
$('#moreButton').on('click', function () { $('#toolsDialog').dialog('open'); });
$('#revisionsButton').on('click', openRevisions);
$('#accessibilityButton').on('click', runAccessibilityCheck);
$('#sharedButton').on('click', openShared);
$('#menusButton').on('click', openMenus);
$('#seoForm').on('submit', applySeo);
$('#scheduleForm').on('submit', schedulePage);
$('#sharedForm').on('submit', saveShared);
$('#menuForm').on('submit', saveMenu);
$('#newPageForm').on('submit', createPage);
$('#pageSearch').on('input', filterPages);
$('#logoutButton').on('click', logout);
$('#closeEditorBtn').on('click', function () { $('#editModal').dialog('close'); });
$('.viewport-button').on('click', function () {
$('#frameContainer').css({width: $(this).data('width'), margin: '0 auto'});
});
}
/**
* Bind editing behaviour whenever the preview iframe loads a page.
* Namespaced events are removed first so repeated navigation cannot multiply handlers.
*/
function initialiseFrame() {
const iframe = document.getElementById('frameContainer');
// Get the iframe document
var iframe = document.getElementById('frameContainer');
iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
currentElement = null;
$(iframeDoc).off('.neocms');
// Editable regions open TinyMCE; generated block controls retain their own click behaviour.
$(iframeDoc).on('click.neocms', editableSelector, function (event) {
if ($(event.target).closest('.button-container').length) return;
// Set up click handler on divs with class 'editable' in the iframe
$(iframeDoc).find('.editable').css('cursor', 'pointer').click(function (event) {
event.preventDefault();
event.stopPropagation();
openEditor($(this));
});
// Internal page navigation receives the same unpublished-change protection as the window.
$(iframeDoc).on('click.neocms', 'a', function (event) {
if (dirty && !window.confirm('Leave this page and discard unpublished changes?')) {
event.preventDefault();
}
});
$(iframeDoc).on('click.neocms', '.duplicate-before, .duplicate-after', duplicateBlock);
$(iframeDoc).on('click.neocms', '.delete-block', deleteBlock);
addBlockControls();
updateUrl();
offerDraft();
}
/** Open TinyMCE with the selected region's inner HTML. */
function openEditor(element) {
currentElement = element;
$('#editor').val(element.html());
$('#editModal').dialog('open');
}
currentDiv = $(this);
/** Initialise TinyMCE after the dialogue is visible and measurable. */
function initialiseEditor() {
// Get the content of the div
var content = currentDiv.html();
// Set the content in TinyMCE
$('#editor').val(content);
// Open the modal
$('#editModal').modal('show');
// Initialize TinyMCE when the modal is shown
$('#editModal').on('shown.bs.modal', function () {
tinymce.init({
selector: '#editor', height: 360, branding: false, promotion: false, license_key: 'gpl',
plugins: 'preview searchreplace autolink autosave directionality code visualblocks visualchars fullscreen image link media codesample table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount quickbars emoticons help',
toolbar: 'undo redo | blocks | bold italic underline | alignleft aligncenter alignright | bullist numlist | link image media table | code preview fullscreen',
images_upload_handler: uploadImage,
promotion: false,
branding: false,
selector: '#editor',
height: 300,
plugins: 'print preview paste importcss searchreplace autolink autosave save directionality code visualblocks visualchars fullscreen image link media template codesample table charmap hr pagebreak nonbreaking anchor toc insertdatetime advlist lists wordcount textpattern noneditable help charmap quickbars emoticons',
toolbar: 'undo redo | bold italic underline strikethrough | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | forecolor backcolor removeformat | pagebreak | charmap emoticons | fullscreen preview save print | insertfile image media template link anchor codesample | ltr rtl',
images_upload_url: '/cms/image_upload.php', // URL of your server-side script
automatic_uploads: true,
convert_urls: false
images_reuse_filename: false,
images_upload_credentials: true,
force_br_newlines: false,
force_p_newlines: false,
forced_root_block: '',
setup: function (editor) {
editor.on('init', function (e) {
editor.setContent(content);
});
}
});
});
/** Remove the editor instance when the dialogue closes. */
function destroyEditor() {
const editor = tinymce.get('editor');
if (editor) editor.remove();
});
});
// Save changes made to a div, but not save to the file on the server
$('#saveBtn').click(function () {
// Get the data from TinyMCE
var data = tinymce.get('editor').getContent();
// Set the data back to the div in the iframe
$(currentDiv).html(data);
// Close the modal
$('#editModal').modal('hide');
});
// Clean up TinyMCE when modal is closed
$('#editModal').on('hidden.bs.modal', function () {
if (tinymce.get('editor')) {
tinymce.get('editor').remove();
}
});
// Handle Saving changes to the page
$('#savePage').click(() => {
$.post("/cms/controller/", {
action: "save",
uri: $('#frameContainer').contents().get(0).location.pathname,
content: new XMLSerializer().serializeToString($('#frameContainer').contents().get(0))
}, function (data) {
// alert(data)
});
});
// Show dialog for page templates
$("#newPage").click(function () {
loadListOfTemplates(); // Call the function to load items when the modal is opened
$("#newPageDialog").dialog({
modal: true,
width: 500,
buttons: {
Close: function () {
$(this).dialog("close");
}
}
});
});
// Handle form submission for creating a new page
$("#newPageForm").submit(function (event) {
event.preventDefault(); // Prevent the default form submission
let selectedItemId = $('input[name="item"]:checked').val();
let filename = $("#filename").val();
/**
* Upload a TinyMCE image with progress reporting and CSRF protection.
*
* @return {Promise<string>} Resolves to the public image URL expected by TinyMCE.
*/
function uploadImage(blobInfo, progress) {
return new Promise(function (resolve, reject) {
const form = new FormData();
form.append('file', blobInfo.blob(), blobInfo.filename());
form.append('csrf_token', csrfToken);
$.ajax({
url: '/cms/image_upload.php', method: 'POST', data: form, processData: false, contentType: false,
xhr: function () {
const xhr = $.ajaxSettings.xhr();
xhr.upload.addEventListener('progress', function (event) {
if (event.lengthComputable) progress(event.loaded / event.total * 100);
});
return xhr;
}
}).done(function (response) { resolve(response.location); })
.fail(function (xhr) { reject((xhr.responseJSON || {}).error || 'Upload failed'); });
});
}
// Apply modal edits to the iframe only; the page remains unpublished until explicitly saved.
$('#saveBtn').on('click', function () {
const editor = tinymce.get('editor');
if (editor && currentElement) {
currentElement.html(editor.getContent());
markDirty();
}
$('#editModal').dialog('close');
});
/** Clone a repeatable block before or after its source, then open the clone for editing. */
function duplicateBlock(event) {
event.preventDefault();
event.stopPropagation();
const source = $(this).closest('.neo-dupe');
const clone = source.clone();
clone.find('.button-container').remove();
if ($(this).hasClass('duplicate-before')) clone.insertBefore(source); else clone.insertAfter(source);
addBlockControls();
markDirty();
openEditor(clone);
}
/** Remove a repeatable block after explicit confirmation. */
function deleteBlock(event) {
event.preventDefault();
event.stopPropagation();
if (window.confirm('Delete this content block?')) {
$(this).closest('.neo-dupe').remove();
markDirty();
}
}
/** Add transient clone and delete controls to every repeatable block in the preview. */
function addBlockControls() {
$(iframeDoc).find('.neo-dupe').each(function () {
const block = $(this);
block.find('.button-container').remove();
// Relative positioning anchors the absolutely positioned controls to this block.
if (block.css('position') === 'static') {
block.attr('data-neo-original-position', 'static').css('position', 'relative');
}
$('<div class="button-container"><button class="duplicate-before" title="Clone before">Before</button><button class="duplicate-after" title="Clone after">After</button><button class="delete-block" title="Delete block">Delete</button></div>').prependTo(block);
});
}
/**
* Clone and serialise the complete page after removing administration-only decorations.
*
* @return {string} A standalone HTML document suitable for drafts or publication.
*/
function serialisePage() {
if (!iframeDoc) throw new Error('No page is loaded');
const clone = iframeDoc.cloneNode(true);
$(clone).find('.button-container').remove();
$(clone).find('[data-neo-original-position="static"]').css('position', '').removeAttr('data-neo-original-position');
$(clone).find(editableSelector + ', .neo-dupe').css('cursor', '');
return '<!DOCTYPE html>\n' + clone.documentElement.outerHTML;
}
/** Return the public path of the page currently displayed in the iframe. */
function currentUri() {
return iframeDoc ? iframeDoc.location.pathname : '/';
}
/** Save the in-memory page as a private server-side draft. */
async function saveDraft() {
try {
const result = await api('saveDraft', {uri: currentUri(), content: serialisePage()}, 'POST');
dirty = false;
showMessage(result.message, 'success');
} catch (error) { showMessage(error.message, 'error'); }
}
/** Run the pre-publish accessibility prompt and publish the complete page. */
async function publishPage() {
if (!permissions.publish) return;
try {
const issues = accessibilityIssues();
if (issues.length && !window.confirm('The accessibility check found ' + issues.length + ' issue(s). Publish anyway?')) return;
const result = await api('save', {uri: currentUri(), content: serialisePage()}, 'POST');
dirty = false;
showMessage(result.message, 'success');
addBlockControls();
} catch (error) { showMessage(error.message, 'error'); }
}
/** Offer to replace the public preview with its most recent saved draft. */
async function offerDraft() {
const uri = currentUri();
if (loadedDrafts.has(uri) || uri.startsWith('/cms/')) return;
loadedDrafts.add(uri);
try {
const draft = await api('getDraft', {uri: uri});
if (draft.exists && window.confirm('A saved draft exists for this page. Load it?')) {
iframeDoc.open(); iframeDoc.write(draft.content); iframeDoc.close();
dirty = true;
}
} catch (error) {
// A page without an available draft needs no noisy notification.
}
}
/** Open the scheduling dialogue with a sensible default of one hour from now. */
function openSchedule() {
const date = new Date(Date.now() + 3600000);
$('#publishAt').val(new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 16));
$('#scheduleDialog').dialog('open');
}
/** Convert the editor's local date to an absolute timestamp and queue the page. */
async function schedulePage(event) {
event.preventDefault();
try {
const result = await api('schedule', {uri: currentUri(), content: serialisePage(), publish_at: new Date($('#publishAt').val()).toISOString()}, 'POST');
dirty = false;
$('#scheduleDialog').dialog('close');
showMessage(result.message, 'success');
} catch (error) { showMessage(error.message, 'error'); }
}
/** Load searchable page metadata and role-appropriate management controls. */
async function openPages() {
try {
const pages = await api('getPages');
const list = $('#fileList').empty();
pages.forEach(function (page) {
const row = $('<li>').attr('data-search', (page.name + ' ' + page.title).toLowerCase());
$('<button class="page-open">').text(page.title + ' - ' + page.name + (page.draft ? ' [draft]' : '')).on('click', function () { navigateTo(page.url); }).appendTo(row);
if (permissions.manage) {
$('<button>').text('Duplicate').on('click', function () { managePage('duplicate', page.url); }).appendTo(row);
$('<button>').text('Rename').on('click', function () { managePage('rename', page.url); }).appendTo(row);
$('<button class="danger-text">').text('Delete').on('click', function () { managePage('delete', page.url); }).appendTo(row);
}
list.append(row);
});
$('#fileListDialog').dialog('open');
} catch (error) { showMessage(error.message, 'error'); }
}
/** Hide page-picker rows that do not contain the case-insensitive search text. */
function filterPages() {
const query = $(this).val().toLowerCase();
$('#fileList li').each(function () { $(this).toggle($(this).data('search').includes(query)); });
}
/** Navigate the preview after protecting any unpublished changes. */
function navigateTo(url) {
if (dirty && !window.confirm('Discard unpublished changes?')) return;
dirty = false;
$('#frameContainer').attr('src', url);
$('#fileListDialog').dialog('close');
}
/** Run an administrator page operation, collecting a target path when required. */
async function managePage(operation, uri) {
let target = '';
if (operation === 'delete') {
if (!window.confirm('Delete ' + uri + '? A revision will be retained.')) return;
url: "/cms/controller/?action=newPage",
method: "POST",
data: {
template: selectedItemId,
filename: filename
},
success: function (response) {
if (typeof response.error != "undefined") {
alert(response.error);
} else {
target = window.prompt('Target path', uri.replace(/\.html?$/, '') + (operation === 'duplicate' ? '-copy.html' : '.html'));
if (!target) return;
$('#frameContainer').contents().get(0).location.href = response.url;
$("#newPageForm").dialog("close"); // Close the modal on success
}
try {
const result = await api('page', {operation: operation, uri: uri, target: target}, 'POST');
showMessage(result.message, 'success');
openPages();
} catch (error) { showMessage(error.message, 'error'); }
},
error: function () {
alert('Failed to create the new page.');
}
/** Load available templates and open the new-page dialogue. */
async function openNewPage() {
try {
const templates = await api('getTemplates');
const list = $('#radioList').empty();
templates.forEach(function (item, index) {
$('<label>').append($('<input type="radio" name="item">').val(item.id).prop('checked', index === 0), document.createTextNode(' ' + item.name)).appendTo(list);
});
$('#newPageDialog').dialog('open');
} catch (error) { showMessage(error.message, 'error'); }
}
/** Create a template-based page and immediately navigate the preview to it. */
async function createPage(event) {
event.preventDefault();
try {
const result = await api('newPage', {template: $('input[name="item"]:checked').val(), filename: $('#filename').val()}, 'POST');
$('#newPageDialog').dialog('close');
navigateTo(result.url);
showMessage(result.message, 'success');
} catch (error) { showMessage(error.message, 'error'); }
}
/** Load the current page's revision history and render restore controls for publishers. */
async function openRevisions() {
try {
const revisions = await api('revisions', {uri: currentUri()});
const list = $('#revisionsList').empty();
if (!revisions.length) list.text('No revisions have been recorded for this page.');
revisions.forEach(function (revision) {
const row = $('<div class="list-row">').text(new Date(revision.created).toLocaleString() + ' - ' + revision.user + ' - ' + revision.reason + ' ');
if (permissions.publish) $('<button>').text('Restore').on('click', function () { restoreRevision(revision.id); }).appendTo(row);
list.append(row);
});
$('#toolsDialog').dialog('close');
$('#revisionsDialog').dialog('open');
} catch (error) { showMessage(error.message, 'error'); }
}
/** Restore and publish a selected revision after confirmation. */
async function restoreRevision(id) {
if (!window.confirm('Restore and publish this revision?')) return;
try {
const result = await api('restoreRevision', {id: id}, 'POST');
dirty = false;
$('#frameContainer').attr('src', result.url + '?neo=' + Date.now());
$('#revisionsDialog').dialog('close');
showMessage(result.message, 'success');
} catch (error) { showMessage(error.message, 'error'); }
}
/** Build the media library with previews, metadata controls, use counts, and deletion. */
async function openMedia() {
try {
const media = await api('media');
const list = $('#mediaList').empty();
media.forEach(function (item) {
const card = $('<div class="media-card">');
$('<img>').attr({src: item.url, alt: item.alt || ''}).on('click', function () { insertMedia(item); }).appendTo(card);
$('<strong>').text(item.name).appendTo(card);
$('<small>').text(formatBytes(item.size) + ' - used ' + item.uses + ' time(s)').appendTo(card);
const alt = $('<input type="text" placeholder="Alternative text">').val(item.alt).appendTo(card);
$('<button>').text('Save alt text').on('click', async function () {
try { showMessage((await api('updateMedia', {name: item.name, alt: alt.val()}, 'POST')).message, 'success'); } catch (error) { showMessage(error.message, 'error'); }
}).appendTo(card);
if (permissions.manage) $('<button class="danger-text">').text('Delete').on('click', function () { deleteMedia(item); }).appendTo(card);
list.append(card);
// Function to load the items via an AJAX GET request and populate the radio buttons for page templates
function loadListOfTemplates() {
$.ajax({
url: "/cms/controller/?action=getTemplates",
method: "GET",
dataType: "json",
success: function (data) {
let radioList = $('#radioList');
radioList.empty(); // Clear previous content
data.forEach(function (item, index) {
radioList.append(`
<label>
<input type="radio" name="item" value="${item.id}" ${index === 0 ? 'checked' : ''}>
${item.name}
</label>
`);
});
$('#mediaDialog').dialog('open');
} catch (error) { showMessage(error.message, 'error'); }
},
error: function () {
alert('Failed to load templates.');
}
/** Append a library image to the currently selected editable region. */
function insertMedia(item) {
if (!currentElement) {
showMessage('Open an editable region first, then select an image.', 'error');
return;
}
currentElement.append($('<img>').attr({src: item.url, alt: item.alt || ''}));
markDirty();
$('#mediaDialog').dialog('close');
}
/** Delete a media file, warning more firmly when public pages still reference it. */
async function deleteMedia(item) {
if (item.uses && !window.confirm('This image is used on ' + item.uses + ' page location(s). Delete it anyway?')) return;
try { showMessage((await api('deleteMedia', {name: item.name}, 'POST')).message, 'success'); openMedia(); } catch (error) { showMessage(error.message, 'error'); }
}
/** Read current document metadata into the SEO form. */
function openSeo() {
if (!iframeDoc) return;
$('#seoTitle').val(iframeDoc.title);
$('#seoDescription').val(metaContent('description'));
$('#seoImage').val(metaProperty('og:image'));
$('#seoCanonical').val($(iframeDoc).find('link[rel="canonical"]').attr('href') || '');
$('#seoNoIndex').prop('checked', /noindex/i.test(metaContent('robots')));
$('#seoDialog').dialog('open');
}
/** Apply SEO form values to the in-memory iframe document. */
function applySeo(event) {
event.preventDefault();
iframeDoc.title = $('#seoTitle').val();
setMeta('name', 'description', $('#seoDescription').val());
setMeta('property', 'og:title', $('#seoTitle').val());
setMeta('property', 'og:description', $('#seoDescription').val());
setMeta('property', 'og:image', $('#seoImage').val());
setMeta('name', 'robots', $('#seoNoIndex').is(':checked') ? 'noindex,nofollow' : 'index,follow');
let canonical = $(iframeDoc).find('link[rel="canonical"]');
if (!canonical.length) canonical = $('<link rel="canonical">').appendTo(iframeDoc.head);
canonical.attr('href', $('#seoCanonical').val());
markDirty();
$('#seoDialog').dialog('close');
showMessage('SEO settings applied. Publish the page to make them live.', 'success');
}
/** Return a named metadata value, or an empty string when the tag is absent. */
function metaContent(name) {
return $(iframeDoc).find('meta[name="' + name + '"]').attr('content') || '';
}
/** Return an Open Graph property value, or an empty string when absent. */
function metaProperty(name) {
return $(iframeDoc).find('meta[property="' + name + '"]').attr('content') || '';
}
/** Create or update one metadata tag in the preview document's head. */
function setMeta(attribute, name, content) {
let tag = $(iframeDoc).find('meta[' + attribute + '="' + name + '"]');
if (!tag.length) tag = $('<meta>').attr(attribute, name).appendTo(iframeDoc.head);
tag.attr('content', content);
}
/** Run the local accessibility checks and present every finding in a dialogue. */
function runAccessibilityCheck() {
const issues = accessibilityIssues();
const results = $('#accessibilityResults').empty();
if (!issues.length) results.append($('<p class="success-text">').text('No common content accessibility problems were found.'));
else issues.forEach(function (issue) { $('<div class="issue">').text(issue).appendTo(results); });
$('#toolsDialog').dialog('close');
$('#accessibilityDialog').dialog('open');
}
/**
* Inspect common authoring concerns that can be checked safely in the browser.
* This is a practical pre-flight check, not a claim of full WCAG conformance.
*/
function accessibilityIssues() {
if (!iframeDoc) return ['No page is loaded.'];
const issues = [];
$(iframeDoc).find('img').each(function (index) { if (!this.hasAttribute('alt')) issues.push('Image ' + (index + 1) + ' has no alt attribute.'); });
$(iframeDoc).find('a').each(function (index) { if (!$(this).text().trim() && !$(this).attr('aria-label')) issues.push('Link ' + (index + 1) + ' has no accessible text.'); });
let previous = 0;
$(iframeDoc).find('h1,h2,h3,h4,h5,h6').each(function () {
const level = Number(this.tagName.slice(1));
if (previous && level > previous + 1) issues.push('Heading level jumps from H' + previous + ' to H' + level + '.');
previous = level;
});
if ($(iframeDoc).find('h1').length !== 1) issues.push('The page should normally contain exactly one H1 heading.');
if (!iframeDoc.title.trim()) issues.push('The page title is empty.');
if (!metaContent('description')) issues.push('The meta description is empty.');
$(iframeDoc).find('input,select,textarea').each(function () { if (!this.id || !$(iframeDoc).find('label[for="' + this.id + '"]').length) issues.push('A form field has no associated label.'); });
return issues;
}
/** Load the shared-content registry and allow an existing block to be selected for editing. */
async function openShared() {
try {
const blocks = await api('shared');
const list = $('#sharedList').empty();
Object.keys(blocks).forEach(function (key) {
$('<button class="list-row">').text(key + ' - updated ' + new Date(blocks[key].updated).toLocaleString()).on('click', function () {
$('#sharedKey').val(key); $('#sharedContent').val(blocks[key].content);
}).appendTo(list);
// Function to load the list of filenames via AJAX
function loadListOfPages() {
$.ajax({
url: "/cms/controller/?action=getPages", // Replace with your actual API endpoint
method: "GET",
dataType: "json",
success: function (data) {
let fileList = $('#fileList');
fileList.empty(); // Clear previous list
data.forEach(function (file) {
fileList.append(`<li data-url="${file.url}">${file.name}</li>`);
});
$('#toolsDialog').dialog('close');
$('#sharedDialog').dialog('open');
} catch (error) { showMessage(error.message, 'error'); }
}
/** Save and propagate a shared block, optionally marking the selected region with its key. */
async function saveShared(event) {
event.preventDefault();
try {
const key = $('#sharedKey').val();
const result = await api('saveShared', {key: key, content: $('#sharedContent').val()}, 'POST');
if (currentElement && window.confirm('Mark the currently selected editable region as this shared block?')) {
currentElement.attr('data-neo-shared', key).html($('#sharedContent').val());
markDirty();
}
showMessage(result.message, 'success');
openShared();
} catch (error) { showMessage(error.message, 'error'); }
}
/** Load named navigation menus and their editable line-based representation. */
async function openMenus() {
try {
const menus = await api('menus');
const list = $('#menuList').empty();
Object.keys(menus).forEach(function (name) {
$('<button class="list-row">').text(name).on('click', function () {
$('#menuName').val(name);
$('#menuItems').val(menus[name].items.map(function (item) { return item.label + ' | ' + item.url + (item.parent ? ' | ' + item.parent : ''); }).join('\n'));
}).appendTo(list);
// Add click event to list items to redirect
$("#fileList li").click(function () {
const fileUrl = $(this).data("url");
$('#frameContainer').contents().get(0).location.href = fileUrl; // Redirect to the clicked file's URL
$("#fileListDialog").dialog("close"); // Close the modal
});
$('#toolsDialog').dialog('close');
$('#menusDialog').dialog('open');
} catch (error) { showMessage(error.message, 'error'); }
},
error: function () {
alert('Failed to load file list.');
}
/** Parse menu lines, save the structure, and optionally insert its rendered navigation. */
async function saveMenu(event) {
event.preventDefault();
const items = $('#menuItems').val().split('\n').map(function (line) {
const parts = line.split('|');
return {label: (parts[0] || '').trim(), url: (parts[1] || '').trim(), parent: (parts[2] || '').trim()};
}).filter(function (item) { return item.url; });
try {
const result = await api('saveMenu', {name: $('#menuName').val(), items: JSON.stringify(items)}, 'POST');
if (currentElement && window.confirm('Insert this menu into the currently selected editable region?')) {
currentElement.html(result.html); markDirty();
}
showMessage(result.message, 'success');
openMenus();
} catch (error) { showMessage(error.message, 'error'); }
}
/**
* Refresh authoritative permissions and dashboard data, optionally opening the dialogue.
* Server permissions control visibility; the server still enforces every operation regardless.
*/
async function loadDashboard(open) {
try {
const dashboard = await api('dashboard');
permissions = dashboard.permissions;
$('.publish-only').toggle(permissions.publish);
$('.manage-only').toggle(permissions.manage);
$('#saveDraft').toggle(permissions.draft);
const content = $('#dashboardContent').empty();
$('<h3>').text('Pending work').appendTo(content);
$('<p>').text(Object.keys(dashboard.drafts).length + ' draft(s), ' + Object.keys(dashboard.schedules).length + ' scheduled publication(s).').appendTo(content);
Object.keys(dashboard.schedules).forEach(function (id) {
const job = dashboard.schedules[id];
const row = $('<div class="list-row">').text(job.uri + ' - ' + new Date(job.publish_at).toLocaleString() + ' ');
if (permissions.schedule) $('<button>').text('Cancel').on('click', function () { cancelSchedule(id); }).appendTo(row);
content.append(row);
});
$('<h3>').text('Recent activity').appendTo(content);
dashboard.activity.forEach(function (entry) { $('<div class="list-row">').text(new Date(entry.created).toLocaleString() + ' - ' + entry.user + ' - ' + entry.action + ': ' + entry.target).appendTo(content); });
if (dashboard.problems.length) {
$('<h3>').text('System problems').appendTo(content);
dashboard.problems.forEach(function (problem) { $('<div class="issue">').text(problem).appendTo(content); });
}
if (open) $('#dashboardDialog').dialog('open');
} catch (error) { showMessage(error.message, 'error'); }
}
/** Cancel one queued publication and refresh the dashboard. */
async function cancelSchedule(id) {
try { showMessage((await api('cancelSchedule', {id: id}, 'POST')).message, 'success'); loadDashboard(true); } catch (error) { showMessage(error.message, 'error'); }
// Handle 'Select Page' button
$("#selectPage").click(function () {
loadListOfPages(); // Call function to load the file list
$("#fileList").dialog({
modal: true,
width: 500,
buttons: {
Close: function () {
$(this).dialog("close");
}
}
});
});
/** End the session after protecting unpublished browser changes. */
async function logout() {
if (dirty && !window.confirm('Log out and discard unpublished changes?')) return;
try { await api('logout', {}, 'POST'); window.location.href = '/cms/login/'; } catch (error) { showMessage(error.message, 'error'); }
}
/** Mark the preview as unpublished and refresh its toolbar status. */
function markDirty() {
dirty = true;
updateUrl();
}
/** Display the active path and whether browser changes remain unpublished. */
function updateUrl() {
$('#urlbox').empty().append($('<strong>').text(dirty ? 'Unpublished: ' : 'Editing: '), $('<span>').text(iframeDoc ? iframeDoc.location.pathname : ''));
}
/** Format a byte count compactly for media cards. */
function formatBytes(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / 1048576).toFixed(1) + ' MB';
}
})(jQuery);
/** Display a temporary, colour-coded status message above the administration interface. */
function showMessage(message, type) {
const bar = jQuery('#message-bar');
bar.stop(true, true).text(message || 'Done').css({backgroundColor: type === 'error' ? '#a51d2d' : '#18733c'}).slideDown();
window.setTimeout(function () { bar.slideUp(); }, 5000);
}
});
+71 -81
View File
@@ -1,100 +1,91 @@
<?php
/** Login form and authentication request handler. */
// Load account configuration before constructing authentication services.
require_once "../config.php";
// Resolve NeoCMS classes without introducing a package-manager requirement.
spl_autoload_register(function ($class) {
$classPath = str_replace('\\', DIRECTORY_SEPARATOR, $class);
require_once "../src/{$classPath}.php";
});
use NeoCMS\Authentication;
use NeoCMS\LoginRateLimiter;
use NeoCMS\Logger;
use NeoCMS\SecurityHeaders;
// Keep one escaped, user-facing error message for the rendered form.
require_once "../neoCMSCore.php";
require_once "../init.php";
// Initialize error message variable
$error = '';
// Authentication owns the session; the logger records successful and failed attempts.
$authentication = new Authentication($config['authentication'] ?? [], $config['roles'] ?? [], $config['security'] ?? []);
$logger = new Logger($config['audit'] ?? true, $config['security'] ?? []);
$dataDirectory = (string) ($config['dataDirectory'] ?? (__DIR__ . '/../data'));
$rateLimiter = new LoginRateLimiter($dataDirectory, $config['security'] ?? []);
SecurityHeaders::html(false, isset($config['security']['cookieSecure']) ? (bool) $config['security']['cookieSecure'] : null);
// GET displays the form, while POST validates its token and submitted credentials.
$maxLoginRequestBytes = 16 * 1024;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > $maxLoginRequestBytes) {
http_response_code(413);
$error = 'Login request is too large';
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
$csrfToken = $_POST['csrf_token'] ?? null;
if (!$authentication->isValidCsrfToken($csrfToken)) {
http_response_code(400);
$logger->write(
'User login attempt rejected due to invalid CSRF token from ' . (string) ($_SERVER['REMOTE_ADDR'] ?? 'unknown'),
'anonymous'
);
$error = "Invalid request token";
} else {
$username = is_string($_POST['username'] ?? null) ? trim($_POST['username']) : '';
$password = is_string($_POST['password'] ?? null) ? $_POST['password'] : '';
$address = (string) ($_SERVER['REMOTE_ADDR'] ?? 'unknown');
if (strlen($username) > 128 || strlen($password) > 4096) {
http_response_code(400);
$error = 'Invalid username or password';
} elseif (($retryAfter = $rateLimiter->retryAfter($address, $username)) > 0) {
http_response_code(429);
header('Retry-After: ' . $retryAfter);
$logger->write("Login throttled from {$address}", $username ?: 'anonymous');
$error = 'Too many login attempts. Please try again later.';
} elseif ($authentication->login($username, $password)) {
$rateLimiter->clear($address, $username);
// Record the source address before entering the privileged interface.
$logger->write(
"User login for {$username} was successful from {$address}",
$username
);
// Redirect after POST so browser refreshes do not resubmit credentials.
header("Location: /cms/");
exit;
} else {
$rateLimiter->recordFailure($address, $username);
// Record denied attempts without logging the supplied password. Obviously.
$logger->write(
"User login for {$username} was denied due to incorrect credentials from {$address}",
$username
);
if (isset($_POST['username']) && isset($_POST['password'])) {
if (!$_SESSION['core']->login($_POST['username'], $_POST['password'])) {
$error = "Invalid username or password";
}
} else {
header("Location:/cms/");
die("Well done, champ! You got this!! <3");
}
}
?>
<!DOCTYPE html>
<html lang="en-gb">
<html>
<head>
<title>NeoCMS Login</title>
<link rel="stylesheet" href="/cms/css/login.css"/>
<title>Login</title>
<style>
/* Basic styling for the login form */
body {
font-family: Arial, sans-serif;
background-color: #eef1f5;
}
.login-container {
width: 350px;
margin: 100px auto;
padding: 40px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.login-container h2 {
text-align: center;
margin-bottom: 30px;
color: #333333;
}
.login-container label {
display: block;
margin-bottom: 8px;
color: #555555;
}
.login-container input[type="text"],
.login-container input[type="password"] {
width: 100%;
padding: 12px;
margin-bottom: 20px;
border: 1px solid #cccccc;
border-radius: 4px;
box-sizing: border-box;
}
.login-container button {
width: 100%;
padding: 12px;
background-color: #0066cc;
border: none;
border-radius: 4px;
color: #ffffff;
font-size: 16px;
cursor: pointer;
}
.login-container button:hover {
background-color: #005bb5;
}
.error-message {
color: #ff4d4d;
margin-bottom: 20px;
text-align: center;
}
</style>
</head>
<body>
<!-- The compact login card is intentionally independent of the heavier administration UI. -->
<div class="login-container">
<img class="logo" src="/cms/img/loginlogo.png" alt="NeoCMS logo"/>
<h2>NeoCMS Login</h2>
<?php if (!empty($error)): ?>
<div class="error-message"><?php echo htmlspecialchars($error, ENT_QUOTES, 'UTF-8'); ?></div>
<div class="error-message"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?>
<!-- CSRF protection applies to login too, preventing forced authentication state changes. -->
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($authentication->getCsrfToken(), ENT_QUOTES, 'UTF-8'); ?>">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autofocus>
@@ -102,7 +93,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (int) ($_SERVER['CONTENT_LENGTH'] ?
<input type="password" id="password" name="password" required>
<button type="submit">Log In</button>
<p class="shamelessPlug">NeoCMS &copy;<?php echo date('Y'); ?> Andy Dixon</p>
</form>
</div>
-2
View File
@@ -1,2 +0,0 @@
# Audit entries contain usernames, addresses, and operational detail; keep them private.
Require all denied
-2
View File
@@ -1,4 +1,2 @@
<?php
// Audit logs are not a public attraction; return to the authenticated CMS instead.
http_response_code(404);
header("Location: /cms/");
+63
View File
@@ -0,0 +1,63 @@
<?php
class neoCMSCore
{
public $auditEnabled = true;
public $users = [];
public $loggedIn = false;
public $version = '2.0';
private $loggedInUser = '';
public function __construct()
{
}
public function login($username, $password)
{
$this->loggedIn = (!empty($this->users[$username]) && $this->users[$username] == $password);
if ($this->loggedIn) $this->loggedInUser = $username;
$this->writeAudit("User login for " . $username . " was " . ($this->loggedIn ? "successful" : "denied due to incorrect credentials") . " from " . $_SERVER['REMOTE_ADDR']);
return $this->loggedIn;
}
/**
* Write an entry into the audit log
* @param $value
* @return void
*/
public function writeAudit($value)
{
if ($this->auditEnabled)
file_put_contents(__DIR__ . "/" . date('y-m-d-') . "audit.txt", date('Y-m-d H:i:s') . "\t" . $this->loggedInUser . "\t" . trim($value) . "\n", FILE_APPEND);
}
public function isLoggedIn()
{
return $this->loggedIn;
}
public function getLoggedinUser()
{
return $this->loggedInUser;
}
public function saveContent($uri, $content)
{
$this->writeAudit("Content for " . $uri . " was saved.");
$uri = str_replace("../", "./", $uri);
//check to see if we're dealing with a default (index.htm{l}) page
if (str_ends_with($uri, "/") || $uri == '/') {
if (file_exists($_SERVER['DOCUMENT_ROOT'] . $uri . 'index.htm')) {
$uri = $uri . '/index.htm';
} elseif (file_exists($_SERVER['DOCUMENT_ROOT'] . $uri . 'index.html')) {
$uri = $uri . '/index.html';
} else {
die('Unknown default file:' . $uri);
}
}
file_put_contents($_SERVER['DOCUMENT_ROOT'] . $uri, $content);
}
}
-32
View File
@@ -1,32 +0,0 @@
<?php
/**
* Command-line worker for scheduled publications.
*
* Run this file from cron once per minute. It is intentionally unavailable over HTTP: scheduled
* publishing is useful, while a public button marked "publish everything due" is rather less so.
*/
// Load the same configuration used by the interactive CMS.
require_once __DIR__ . '/config.php';
// Register the small project autoloader because NeoCMS deliberately has no Composer dependency.
spl_autoload_register(function ($class) {
$classPath = str_replace('\\', DIRECTORY_SEPARATOR, $class);
require_once __DIR__ . "/src/{$classPath}.php";
});
// Refuse web requests even if the web server accidentally exposes this script.
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
// Cron may omit DOCUMENT_ROOT, so derive it from the CMS directory when necessary.
if (empty($_SERVER['DOCUMENT_ROOT'])) {
$_SERVER['DOCUMENT_ROOT'] = dirname(__DIR__);
}
// Publish every due job and print a concise result suitable for a cron log.
$controller = new NeoCMS\CMSController($config ?? []);
$count = $controller->publishScheduled();
echo "Published {$count} scheduled page(s).\n";
-255
View File
@@ -1,255 +0,0 @@
<?php
namespace NeoCMS;
/**
* Manages session authentication, role capabilities, and CSRF tokens.
*
* Credentials remain configuration-backed to preserve NeoCMS's database-free design. Only hashes
* created by password_hash() are accepted; plaintext credentials are rejected.
*/
class Authentication
{
/** Valid bcrypt hash used solely to equalise unknown-user password verification work. */
private const DUMMY_PASSWORD_HASH = '$2y$10$NaiFDtsxcQzcxAiQrTmEPuDde8sDsdpjAlSgoiYwAvdnF5Cwytute';
/** Map of usernames to password hashes. */
private array $credentials;
/** Map of usernames to editor, publisher, or administrator roles. */
private array $roles;
/** Maximum permitted inactivity for an authenticated session. */
private int $idleTimeout;
/** Maximum total lifetime of an authenticated session. */
private int $absoluteTimeout;
/** Optional explicit Secure-cookie setting for TLS-terminating reverse proxies. */
private ?bool $cookieSecure;
/** Initialise secure session handling and retain the configured account maps. */
public function __construct(array $credentials, array $roles = [], array $sessionOptions = [])
{
$this->credentials = $credentials;
$this->roles = $roles;
$this->idleTimeout = max(300, (int) ($sessionOptions['idleTimeout'] ?? 1800));
$this->absoluteTimeout = max($this->idleTimeout, (int) ($sessionOptions['absoluteTimeout'] ?? 43200));
$this->cookieSecure = isset($sessionOptions['cookieSecure']) ? (bool) $sessionOptions['cookieSecure'] : null;
$this->startSession();
$this->enforceSessionLifetime();
}
/**
* Start the PHP session with conservative cookie settings.
*
* HTTPS cookies are marked Secure when the request is encrypted. SameSite=Lax protects common
* cross-site request scenarios without making ordinary login redirects needlessly dramatic.
*/
private function startSession(): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}
$isHttps = $this->cookieSecure ?? (
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443)
);
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_samesite', 'Lax');
session_name('NEOCMSSESSID');
session_set_cookie_params([
'httponly' => true,
'path' => '/cms',
'samesite' => 'Lax',
'secure' => $isHttps,
]);
session_start();
}
/**
* Authenticate a user and establish a fresh privileged session.
*
* @param mixed $username Submitted account name.
* @param mixed $password Submitted plaintext password.
*/
public function login($username, $password): bool
{
$username = (string) $username;
if ($this->credentialsAreValid($username, (string) $password)) {
// Rotate the identifier at the privilege boundary to prevent session fixation.
session_regenerate_id(true);
$_SESSION['loggedIn'] = true;
$_SESSION['loggedInUser'] = $username;
$configuredRole = $this->roles[$username] ?? 'editor';
$_SESSION['role'] = in_array($configuredRole, ['editor', 'publisher', 'administrator'], true)
? $configuredRole
: 'editor';
$_SESSION['credentialFingerprint'] = hash('sha256', (string) $this->credentials[$username]);
$_SESSION['csrfToken'] = bin2hex(random_bytes(32));
$_SESSION['authenticatedAt'] = time();
$_SESSION['lastActivityAt'] = time();
$_SESSION['lastRegeneratedAt'] = time();
return true;
}
unset(
$_SESSION['loggedIn'],
$_SESSION['loggedInUser'],
$_SESSION['role'],
$_SESSION['credentialFingerprint'],
$_SESSION['authenticatedAt'],
$_SESSION['lastActivityAt'],
$_SESSION['lastRegeneratedAt']
);
return false;
}
/** Return whether the current session belongs to an authenticated user. */
public function isLoggedIn(): bool
{
return $_SESSION['loggedIn'] ?? false;
}
/** Return the current username, or a safe label when no user is authenticated. */
public function getLoggedInUser(): string
{
return $_SESSION['loggedInUser'] ?? 'Not logged in';
}
/** Return the session role, defaulting unauthorised sessions to the least powerful role. */
public function getRole(): string
{
$username = $this->getLoggedInUser();
$configuredRole = $this->roles[$username] ?? 'editor';
$role = in_array($configuredRole, ['editor', 'publisher', 'administrator'], true)
? $configuredRole
: 'editor';
$_SESSION['role'] = $role;
return $role;
}
/**
* Check whether the current role includes a named capability.
*
* Capability checks live here so controllers and upload handlers share one policy table.
*/
public function can(string $capability): bool
{
$permissions = [
'editor' => ['draft', 'upload'],
'publisher' => ['draft', 'upload', 'publish', 'schedule'],
'administrator' => ['draft', 'upload', 'publish', 'schedule', 'manage'],
];
return in_array($capability, $permissions[$this->getRole()] ?? [], true);
}
/** Return the session's CSRF token, generating a cryptographically random token when absent. */
public function getCsrfToken(): string
{
if (empty($_SESSION['csrfToken'])) {
$_SESSION['csrfToken'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrfToken'];
}
/** Compare a submitted CSRF token in constant time to avoid leaking useful timing information. */
public function isValidCsrfToken(?string $token): bool
{
if (!is_string($token) || $token === '') {
return false;
}
return hash_equals($this->getCsrfToken(), $token);
}
/** Clear session data, expire its cookie, and destroy the server-side session. */
public function logout(): void
{
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', [
'expires' => time() - 42000,
'path' => $params['path'],
'domain' => $params['domain'],
'secure' => $params['secure'],
'httponly' => $params['httponly'],
'samesite' => 'Lax',
]);
}
session_destroy();
}
/**
* Validate submitted credentials against a password_hash() value.
*/
private function credentialsAreValid(string $username, string $password): bool
{
$storedCredential = $this->credentials[$username] ?? null;
$passwordInfo = is_string($storedCredential) ? password_get_info($storedCredential) : [];
$configuredHashIsValid = is_string($storedCredential)
&& $storedCredential !== ''
&& ($passwordInfo['algoName'] ?? 'unknown') !== 'unknown';
$verificationHash = $configuredHashIsValid ? $storedCredential : self::DUMMY_PASSWORD_HASH;
$verified = password_verify($password, $verificationHash);
return $configuredHashIsValid && $verified;
}
/** Expire stale sessions and periodically rotate active session identifiers. */
private function enforceSessionLifetime(): void
{
if (empty($_SESSION['loggedIn'])) {
return;
}
$now = time();
$username = is_string($_SESSION['loggedInUser'] ?? null) ? $_SESSION['loggedInUser'] : '';
$storedCredential = $this->credentials[$username] ?? null;
$passwordInfo = is_string($storedCredential) ? password_get_info($storedCredential) : [];
if (!is_string($storedCredential) || ($passwordInfo['algoName'] ?? 'unknown') === 'unknown') {
$this->invalidateSession();
return;
}
$fingerprint = hash('sha256', $storedCredential);
if (isset($_SESSION['credentialFingerprint']) && !hash_equals((string) $_SESSION['credentialFingerprint'], $fingerprint)) {
$this->invalidateSession();
return;
}
$_SESSION['credentialFingerprint'] = $fingerprint;
$_SESSION['authenticatedAt'] = (int) ($_SESSION['authenticatedAt'] ?? $now);
$_SESSION['lastActivityAt'] = (int) ($_SESSION['lastActivityAt'] ?? $now);
$_SESSION['lastRegeneratedAt'] = (int) ($_SESSION['lastRegeneratedAt'] ?? $now);
$idleExpired = $now - $_SESSION['lastActivityAt'] > $this->idleTimeout;
$absoluteExpired = $now - $_SESSION['authenticatedAt'] > $this->absoluteTimeout;
if ($idleExpired || $absoluteExpired) {
$this->invalidateSession();
return;
}
if ($now - $_SESSION['lastRegeneratedAt'] > 900) {
session_regenerate_id(true);
$_SESSION['lastRegeneratedAt'] = $now;
}
$_SESSION['lastActivityAt'] = $now;
}
/** Clear privileged session state and rotate the identifier after expiry or account revocation. */
private function invalidateSession(): void
{
$_SESSION = [];
session_regenerate_id(true);
}
}
File diff suppressed because it is too large Load Diff
-114
View File
@@ -1,114 +0,0 @@
<?php
namespace NeoCMS;
/**
* Persists NeoCMS metadata as human-readable JSON and provides managed subdirectories.
*
* Public pages remain ordinary HTML files. This store contains only the supporting cast: drafts,
* revisions, schedules, menus, media metadata, and activity. No database server need feel left out.
*/
final class FileStore
{
/** Absolute data-directory path, always ending with the platform separator. */
private string $root;
/**
* Prepare the configured data directory.
*
* @param string $root Absolute or project-relative storage path.
*/
public function __construct(string $root)
{
$this->root = rtrim($root, '/\\') . DIRECTORY_SEPARATOR;
$this->ensureDirectory($this->root);
}
/**
* Read a named JSON document, returning the supplied default when it does not yet exist.
* Invalid JSON is treated as absent data so callers always receive the promised array shape.
*
* @param string $name Logical document name without the .json suffix.
* @param array $default Value returned for a missing or unreadable document.
* @return array Decoded document contents.
*/
public function read(string $name, array $default = []): array
{
$path = $this->jsonPath($name);
if (!is_file($path)) {
return $default;
}
$value = json_decode((string) file_get_contents($path), true);
return is_array($value) ? $value : $default;
}
/**
* Atomically replace a named JSON document.
*
* Data is written to a temporary neighbour and renamed only after the write succeeds. Readers
* therefore see either the old document or the complete new one, never half a JSON sandwich.
*
* @param string $name Logical document name without the .json suffix.
* @param array $value Serializable document contents.
*/
public function write(string $name, array $value): void
{
$path = $this->jsonPath($name);
$temporary = $path . '.' . bin2hex(random_bytes(6)) . '.tmp';
$json = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if ($json === false || file_put_contents($temporary, $json . "\n", LOCK_EX) === false) {
throw new \RuntimeException('Unable to write CMS data');
}
@chmod($temporary, 0600);
if (!rename($temporary, $path)) {
@unlink($temporary);
throw new \RuntimeException('Unable to replace CMS data');
}
@chmod($path, 0600);
}
/** Atomically write a private content file beneath a managed subdirectory. */
public function writePrivateFile(string $directory, string $filename, string $content): string
{
$target = $this->directory($directory) . basename($filename);
$temporary = $target . '.' . bin2hex(random_bytes(6)) . '.tmp';
if (file_put_contents($temporary, $content, LOCK_EX) === false) {
throw new \RuntimeException('Unable to write private CMS file');
}
@chmod($temporary, 0600);
if (!rename($temporary, $target)) {
@unlink($temporary);
throw new \RuntimeException('Unable to replace private CMS file');
}
@chmod($target, 0600);
return $target;
}
/**
* Return a managed subdirectory, creating it when first required.
*
* @param string $name Relative directory name such as drafts or revisions.
*/
public function directory(string $name): string
{
$path = $this->root . trim($name, '/\\') . DIRECTORY_SEPARATOR;
$this->ensureDirectory($path);
return $path;
}
/** Convert a logical document name into a safe JSON path beneath the data root. */
private function jsonPath(string $name): string
{
return $this->root . preg_replace('/[^a-zA-Z0-9_.-]/', '', $name) . '.json';
}
/** Create a directory recursively or fail with a useful application-level exception. */
private function ensureDirectory(string $path): void
{
if (!is_dir($path) && !mkdir($path, 0700, true) && !is_dir($path)) {
throw new \RuntimeException('Unable to create CMS data directory');
}
@chmod($path, 0700);
}
}
-120
View File
@@ -1,120 +0,0 @@
<?php
namespace NeoCMS;
/** Writes simple, daily, append-only audit logs for operational traceability. */
class Logger
{
/** Whether audit writes are enabled by configuration. */
private $auditEnabled;
/** Absolute directory containing the dated audit files. */
private $logsDir;
/** Maximum size of one active daily log before it is rotated. */
private $maxFileBytes;
/** Number of days for which audit logs are retained. */
private $retentionDays;
/**
* Configure logging and ensure its destination exists.
*
* @param mixed $auditEnabled Truthy values enable audit output.
* @param array<string, mixed> $security Security limits shared with the rest of the CMS.
*/
public function __construct($auditEnabled, array $security = [])
{
$this->auditEnabled = $auditEnabled;
// Conservative defaults prevent a forgotten audit directory from becoming its own archive service.
$this->maxFileBytes = max(1024, (int) ($security['auditMaxFileBytes'] ?? 10 * 1024 * 1024));
$this->retentionDays = max(1, (int) ($security['auditRetentionDays'] ?? 90));
$this->logsDir = __DIR__ . '/../../logs';
$this->logsDir = rtrim($this->logsDir, '/\\') . DIRECTORY_SEPARATOR;
// Create the directory lazily so a fresh installation needs no ceremonial folder setup.
if (!is_dir($this->logsDir)) {
if (!mkdir($this->logsDir, 0700, true)) {
throw new \Exception('Failed to create logs directory: ' . $this->logsDir);
}
}
@chmod($this->logsDir, 0700);
if ($this->auditEnabled) {
$this->pruneExpiredLogs();
}
}
/**
* Append a timestamped user action to today's audit file.
*
* @param mixed $message Human-readable action description.
* @param mixed $user Username responsible for the action.
*/
public function write($message, $user): void
{
if (!$this->auditEnabled) {
return;
}
// One file per day keeps inspection and rotation pleasantly unsurprising.
$logFile = $this->logsDir . date('Y-m-d') . '-audit.txt';
$this->rotateFullLog($logFile);
$entry = sprintf(
"[%s]\tUser: %s\t%s\n",
date('Y-m-d H:i:s'),
$this->normaliseLogField($user),
$this->normaliseLogField($message)
);
if (file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX) === false) {
throw new \RuntimeException('Unable to write audit log');
}
@chmod($logFile, 0600);
}
/**
* Move a full daily log aside before appending another entry.
*
* @param string $logFile Absolute path to today's active log.
*/
private function rotateFullLog(string $logFile): void
{
clearstatcache(true, $logFile);
if (!is_file($logFile) || (int) filesize($logFile) < $this->maxFileBytes) {
return;
}
$rotated = $this->logsDir . date('Y-m-d-His') . '-' . bin2hex(random_bytes(3)) . '-audit.txt';
if (!rename($logFile, $rotated)) {
throw new \RuntimeException('Unable to rotate audit log');
}
@chmod($rotated, 0600);
}
/** Remove audit files older than the configured retention period. */
private function pruneExpiredLogs(): void
{
$cutoff = time() - ($this->retentionDays * 86400);
$logFiles = glob($this->logsDir . '*-audit.txt') ?: [];
foreach ($logFiles as $logFile) {
if (is_file($logFile) && (int) filemtime($logFile) < $cutoff && !unlink($logFile)) {
throw new \RuntimeException('Unable to remove expired audit log');
}
}
}
/**
* Remove control characters that could forge extra audit entries or columns.
*
* @param mixed $value Value destined for one tab-separated log field.
*/
private function normaliseLogField($value): string
{
$value = trim((string) $value);
return str_replace(["\r", "\n", "\t"], [' ', ' ', ' '], $value);
}
}
-142
View File
@@ -1,142 +0,0 @@
<?php
namespace NeoCMS;
/**
* Provides filesystem-backed throttling for failed login attempts.
*
* Limits are applied to both the source address and the source-address/username pair. This slows
* password guessing without allowing one remote address to lock an account for everybody else.
*/
final class LoginRateLimiter
{
private string $path;
private int $windowSeconds;
private int $lockoutSeconds;
private int $maxIdentityAttempts;
private int $maxAddressAttempts;
/** Configure the protected state file and throttling thresholds. */
public function __construct(string $dataDirectory, array $options = [])
{
$directory = rtrim($dataDirectory, '/\\') . DIRECTORY_SEPARATOR;
if (!is_dir($directory) && !mkdir($directory, 0700, true) && !is_dir($directory)) {
throw new \RuntimeException('Unable to create login security directory');
}
@chmod($directory, 0700);
$this->path = $directory . 'login-attempts.json';
$this->windowSeconds = max(60, (int) ($options['loginWindowSeconds'] ?? 900));
$this->lockoutSeconds = max(60, (int) ($options['loginLockoutSeconds'] ?? 900));
$this->maxIdentityAttempts = max(1, (int) ($options['loginMaxAttempts'] ?? 5));
$this->maxAddressAttempts = max($this->maxIdentityAttempts, (int) ($options['loginMaxAddressAttempts'] ?? 20));
}
/** Return the remaining lockout in seconds, or zero when another attempt is permitted. */
public function retryAfter(string $address, string $username): int
{
return $this->withState(function (array &$state) use ($address, $username): int {
$now = time();
$this->prune($state, $now);
$retryAt = max(
(int) ($state[$this->addressKey($address)]['locked_until'] ?? 0),
(int) ($state[$this->identityKey($address, $username)]['locked_until'] ?? 0)
);
return max(0, $retryAt - $now);
});
}
/** Record one denied login and lock a bucket once its configured threshold is reached. */
public function recordFailure(string $address, string $username): void
{
$this->withState(function (array &$state) use ($address, $username): void {
$now = time();
$this->prune($state, $now);
$this->increment($state, $this->addressKey($address), $this->maxAddressAttempts, $now);
$this->increment($state, $this->identityKey($address, $username), $this->maxIdentityAttempts, $now);
});
}
/** Clear successful login buckets so typographical mistakes do not linger unnecessarily. */
public function clear(string $address, string $username): void
{
$this->withState(function (array &$state) use ($address, $username): void {
unset($state[$this->addressKey($address)], $state[$this->identityKey($address, $username)]);
$this->prune($state, time());
});
}
/** Update one attempt bucket and begin a fixed lockout when its threshold is reached. */
private function increment(array &$state, string $key, int $limit, int $now): void
{
$record = $state[$key] ?? ['attempts' => [], 'locked_until' => 0];
$record['attempts'][] = $now;
if (count($record['attempts']) >= $limit) {
$record['locked_until'] = $now + $this->lockoutSeconds;
$record['attempts'] = [];
}
$state[$key] = $record;
}
/** Remove expired attempts and stale unlocked buckets to keep the state file bounded. */
private function prune(array &$state, int $now): void
{
$cutoff = $now - $this->windowSeconds;
foreach ($state as $key => &$record) {
$record['attempts'] = array_values(array_filter(
is_array($record['attempts'] ?? null) ? $record['attempts'] : [],
static fn($timestamp): bool => is_int($timestamp) && $timestamp >= $cutoff
));
$record['locked_until'] = (int) ($record['locked_until'] ?? 0);
if ($record['attempts'] === [] && $record['locked_until'] <= $now) {
unset($state[$key]);
}
}
unset($record);
}
/** Serialise state updates beneath an exclusive lock to avoid concurrent lost updates. */
private function withState(callable $callback)
{
$handle = fopen($this->path, 'c+');
if ($handle === false || !flock($handle, LOCK_EX)) {
if (is_resource($handle)) {
fclose($handle);
}
throw new \RuntimeException('Unable to access login security state');
}
@chmod($this->path, 0600);
try {
rewind($handle);
$decoded = json_decode((string) stream_get_contents($handle), true);
$state = is_array($decoded) ? $decoded : [];
$result = $callback($state);
$json = json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if ($json === false) {
throw new \RuntimeException('Unable to encode login security state');
}
rewind($handle);
if (!ftruncate($handle, 0) || fwrite($handle, $json . "\n") === false || !fflush($handle)) {
throw new \RuntimeException('Unable to write login security state');
}
@chmod($this->path, 0600);
return $result;
} finally {
flock($handle, LOCK_UN);
fclose($handle);
}
}
/** Hash address data so the throttling file does not become a convenient IP-address register. */
private function addressKey(string $address): string
{
return 'address:' . hash('sha256', $address);
}
/** Hash the combined address and case-normalised username for the tighter identity bucket. */
private function identityKey(string $address, string $username): string
{
return 'identity:' . hash('sha256', $address . "\0" . strtolower($username));
}
}
-53
View File
@@ -1,53 +0,0 @@
<?php
namespace NeoCMS;
/** Applies consistent browser security and cache-control headers to CMS responses. */
final class SecurityHeaders
{
/** Protect an HTML administration page, optionally allowing the pinned UI CDNs. */
public static function html(bool $allowUiCdns = false, ?bool $secureTransport = null): void
{
self::common($secureTransport);
header('Cache-Control: no-store, private');
header('Pragma: no-cache');
$scriptSources = $allowUiCdns
? "'self' https://code.jquery.com"
: "'self'";
$styleSources = $allowUiCdns
? "'self' 'unsafe-inline' https://code.jquery.com"
: "'self'";
header("Content-Security-Policy: default-src 'self'; script-src {$scriptSources}; style-src {$styleSources}; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self'; frame-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'");
}
/** Protect JSON API responses and prevent authenticated data from being cached. */
public static function json(?bool $secureTransport = null): void
{
self::common($secureTransport);
header('Cache-Control: no-store, private');
header('Pragma: no-cache');
header("Content-Security-Policy: default-src 'none'; frame-ancestors 'none'");
header('Content-Type: application/json');
}
/** Emit headers shared by both document and API responses. */
private static function common(?bool $secureTransport): void
{
header_remove('X-Powered-By');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Referrer-Policy: no-referrer');
header('Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()');
header('Cross-Origin-Opener-Policy: same-origin');
header('Cross-Origin-Resource-Policy: same-origin');
$isHttps = $secureTransport ?? (
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443)
);
if ($isHttps) {
header('Strict-Transport-Security: max-age=31536000');
}
}
}
+16 -14
View File
@@ -1,24 +1,26 @@
<!DOCTYPE html>
<html lang="en-gb">
<html lang="en">
<head>
<!-- A repeatable-content starter template for listings, teams, services, and similar pages. -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>New NeoCMS Listing</title>
<meta name="description" content="Describe this listing for visitors and search engines.">
<title>Welcome to NeoCMS</title>
<link href="/cms/css/welcome.style.css" rel="stylesheet">
</head>
<body>
<header class="editable">
<h1>New listing</h1>
<p>Edit this introduction, then duplicate the example item below as required.</p>
<header>
<img alt="Application Logo" class="logo" src="/cms/img/welcomeLogo.png">
</header>
<main>
<!-- neo-dupe adds clone-before, clone-after, and delete controls in the CMS preview. -->
<article class="editable neo-dupe">
<h2>Example item</h2>
<p>Replace this content or duplicate the complete item.</p>
</article>
<h1>Welcome to NeoCMS</h1>
<p>This is another example template, found in /cms/templates/ - remove me and replace me with your own site
templates!</p>
<p class="editable">
This box is editable!<br/>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Quisque sit amet accumsan tortor. Itaque earum rerum hic tenetur a sapiente delectus.
</p>
</main>
<footer>
<p>&copy; 2024 Andy Dixon</p>
</footer>
</body>
</html>
+17 -13
View File
@@ -1,22 +1,26 @@
<!DOCTYPE html>
<html lang="en-gb">
<html lang="en">
<head>
<!-- Replace these metadata values through the CMS SEO dialogue after creating the page. -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>New NeoCMS Page</title>
<meta name="description" content="Describe this page for visitors and search engines.">
<title>Welcome to NeoCMS</title>
<link href="/cms/css/welcome.style.css" rel="stylesheet">
</head>
<body>
<!-- The default editable class may be changed with editableClass in cms/config.php. -->
<main class="editable">
<h1>New page</h1>
<p>Select this region in NeoCMS to replace the example content.</p>
<header>
<img alt="Application Logo" class="logo" src="/cms/img/welcomeLogo.png">
</header>
<main>
<h1>Welcome to NeoCMS</h1>
<p>This is an example template, found in /cms/templates/ - remove me and replace me with your own site
templates!</p>
<p class="editable">
This box is editable!<br/>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Quisque sit amet accumsan tortor. Itaque earum rerum hic tenetur a sapiente delectus.
</p>
</main>
<!-- Shared keys allow one saved block to update every matching page. -->
<footer class="editable" data-neo-shared="site-footer">
<p>Shared site footer</p>
<footer>
<p>&copy; 2024 Andy Dixon</p>
</footer>
</body>
</html>
File diff suppressed because it is too large Load Diff
-77
View File
@@ -1,77 +0,0 @@
# TinyMCE
The world's #1 open source rich text editor.
**Using an old version of TinyMCE?** We recommend you to upgrade to TinyMCE 8 to continue receiving security updates.
Used and trusted by millions of developers, TinyMCE is the world’s most customizable, scalable, and flexible rich text editor. We’ve helped launch the likes of Atlassian, Medium, Evernote (and lots more that we can’t tell you), by empowering them to create exceptional content and experiences for their users.
With more than 350M+ downloads every year, we’re also one of the most trusted enterprise-grade open source HTML editors on the internet. There’s currently more than 100M+ products worldwide, powered by Tiny. As a high powered WYSIWYG editor, TinyMCE is built to scale, designed to innovate, and thrives on delivering results to difficult edge-cases.
You can access a [full featured demo of TinyMCE](https://www.tiny.cloud/docs/tinymce/8/premium-full-featured/) in the docs on the TinyMCE website.
<p align="center">
<img alt="Screenshot of the TinyMCE Editor" src="https://www.tiny.cloud/storage/github-readme-images/tinymce-editor-6x.png"\>
</p>
## Get started with TinyMCE
Getting started with the TinyMCE rich text editor is easy, and for simple configurations can be done in less than 5 minutes.
[TinyMCE Cloud Deployment Quick Start Guide](https://www.tiny.cloud/docs/tinymce/8/cloud-quick-start/)
[TinyMCE Self-hosted Deployment Guide](https://www.tiny.cloud/docs/tinymce/8/npm-projects/)
TinyMCE provides a range of configuration options that allow you to integrate it into your application. Start customizing with a [basic setup](https://www.tiny.cloud/docs/tinymce/8/basic-setup/).
Configure it for one of three modes of editing:
- [TinyMCE classic editing mode](https://www.tiny.cloud/docs/tinymce/8/use-tinymce-classic/).
- [TinyMCE inline editing mode](https://www.tiny.cloud/docs/tinymce/8/use-tinymce-inline/).
- [TinyMCE distraction-free editing mode](https://www.tiny.cloud/docs/tinymce/8/use-tinymce-distraction-free/).
## Features
### Integration
TinyMCE is easily integrated into your projects with the help of components such as:
- [tinymce-react](https://github.com/tinymce/tinymce-react)
- [tinymce-vue](https://github.com/tinymce/tinymce-vue)
- [tinymce-angular](https://github.com/tinymce/tinymce-angular)
With over 29 integrations, and 400+ APIs, see the TinyMCE docs for a full list of editor [integrations](https://www.tiny.cloud/docs/tinymce/8/integrations/).
### Customization
It is easy to [configure the UI](https://www.tiny.cloud/docs/tinymce/8/customize-ui/) of your rich text editor to match the design of your site, product or application. Due to its flexibility, you can [configure the editor](https://www.tiny.cloud/docs/tinymce/8/basic-setup/) with as much or as little functionality as you like, depending on your requirements.
With [50+ powerful plugins available](https://www.tiny.cloud/tinymce/features/), and content editable as the basis of TinyMCE, adding additional functionality is as simple as including a single line of code.
Realizing the full power of most plugins requires only a few lines more.
### Extensibility
Sometimes your editor requirements can be quite unique, and you need the freedom and flexibility to innovate. Thanks to TinyMCE being open source, you can view the source code and develop your own extensions for custom functionality to meet your own requirements.
The TinyMCE [API](https://www.tiny.cloud/docs/tinymce/8/apis/tinymce.root/) is exposed to make it easier for you to write custom functionality that fits within the existing framework of TinyMCE [UI components](https://www.tiny.cloud/docs/tinymce/8/custom-ui-components/).
### Extended Features and Support
For the professional software teams that require more in-depth efficiency, compliance or collaborative features built to enterprise-grade standards, please [get in touch with our team](https://www.tiny.cloud/contact/).
Tiny also offers dedicated SLAs and support for professional development teams.
## Compiling and contributing
In 2019 the decision was made to transition our codebase to a monorepo. For information on compiling and contributing, see: [contribution guidelines](https://github.com/tinymce/tinymce/blob/master/CONTRIBUTING.md).
As an open source product, we encourage and support the active development of our software.
## Want more information?
Visit the [TinyMCE website](https://tiny.cloud/) and check out the [TinyMCE documentation](https://www.tiny.cloud/docs/).
## License
License terms can be found in the license.md file.
-27
View File
@@ -1,27 +0,0 @@
{
"name": "tinymce",
"description": "Web based JavaScript HTML WYSIWYG editor control.",
"license": "SEE LICENSE IN license.md",
"keywords": [
"wysiwyg",
"tinymce",
"richtext",
"javascript",
"html",
"text",
"rich editor",
"rich text editor",
"rte",
"rich text",
"contenteditable",
"editing"
],
"homepage": "https://www.tiny.cloud/",
"ignore": [
"README.md",
"composer.json",
"package.json",
".npmignore",
"CHANGELOG.md"
]
}
-52
View File
@@ -1,52 +0,0 @@
{
"name": "tinymce/tinymce",
"version": "8.5.1",
"description": "Web based JavaScript HTML WYSIWYG editor control.",
"license": [
"SEE LICENSE IN license.md"
],
"keywords": [
"wysiwyg",
"tinymce",
"richtext",
"javascript",
"html",
"text",
"rich editor",
"rich text editor",
"rte",
"rich text",
"contenteditable",
"editing"
],
"homepage": "https://www.tiny.cloud/",
"type": "component",
"extra": {
"component": {
"scripts": [
"tinymce.js",
"plugins/*/plugin.js",
"themes/*/theme.js",
"models/*/model.js",
"icons/*/icons.js"
],
"files": [
"tinymce.min.js",
"plugins/*/plugin.min.js",
"themes/*/theme.min.js",
"models/*/model.min.js",
"skins/**",
"icons/*/icons.min.js"
]
}
},
"archive": {
"exclude": [
"README.md",
"bower.js",
"package.json",
".npmignore",
"CHANGELOG.md"
]
}
}
-270
View File
@@ -1,270 +0,0 @@
tinymce.IconManager.add('default', {
icons: {
'accessibility-check': '<svg width="24" height="24"><path d="M12 2a2 2 0 0 1 2 2 2 2 0 0 1-2 2 2 2 0 0 1-2-2c0-1.1.9-2 2-2Zm8 7h-5v12c0 .6-.4 1-1 1a1 1 0 0 1-1-1v-5c0-.6-.4-1-1-1a1 1 0 0 0-1 1v5c0 .6-.4 1-1 1a1 1 0 0 1-1-1V9H4a1 1 0 1 1 0-2h16c.6 0 1 .4 1 1s-.4 1-1 1Z" fill-rule="nonzero"/></svg>',
'accordion-toggle': '<svg width="24" height="24"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 15c0-.6.4-1 1-1h6c.6 0 1 .4 1 1s-.4 1-1 1h-6a1 1 0 0 1-1-1Z"/><path opacity=".2" fill-rule="evenodd" clip-rule="evenodd" d="M4 15c0-.6.4-1 1-1h6c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1-1-1Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12 19c0-.6.4-1 1-1h6c.6 0 1 .4 1 1s-.4 1-1 1h-6a1 1 0 0 1-1-1Z"/><path opacity=".2" fill-rule="evenodd" clip-rule="evenodd" d="M4 19c0-.6.4-1 1-1h6c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1-1-1Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12.3 7.3a1 1 0 0 1 1.4 0L16 9.6l2.3-2.3a1 1 0 1 1 1.4 1.4L16 12.4l-3.7-3.7a1 1 0 0 1 0-1.4ZM4.3 11.7a1 1 0 0 1 0-1.4L6.6 8 4.3 5.7a1 1 0 0 1 1.4-1.4L9.4 8l-3.7 3.7a1 1 0 0 1-1.4 0Z"/></svg>',
'accordion': '<svg width="24" height="24"><rect x="12" y="7" width="10" height="2" rx="1"/><rect x="12" y="11" width="10" height="2" rx="1"/><rect x="12" y="15" width="6" height="2" rx="1"/><path fill-rule="evenodd" clip-rule="evenodd" d="M2.3 7.3a1 1 0 0 1 1.4 0L6 9.6l2.3-2.3a1 1 0 0 1 1.4 1.4L6 12.4 2.3 8.7a1 1 0 0 1 0-1.4Z"/></svg>',
'action-next': '<svg width="24" height="24"><path fill-rule="nonzero" d="M5.7 7.3a1 1 0 0 0-1.4 1.4l7.7 7.7 7.7-7.7a1 1 0 1 0-1.4-1.4L12 13.6 5.7 7.3Z"/></svg>',
'action-prev': '<svg width="24" height="24"><path fill-rule="nonzero" d="M18.3 15.7a1 1 0 0 0 1.4-1.4L12 6.6l-7.7 7.7a1 1 0 0 0 1.4 1.4L12 9.4l6.3 6.3Z"/></svg>',
'add-file': '<svg height="24" width="24"><path d="M2 7h2V4h3V2H4a2 2 0 0 0-2 2zm20 0h-2V4h-3V2h3a2 2 0 0 1 2 2zm0 2h-2v6h2zm0 8h-2v3h-3v2h3a2 2 0 0 0 2-2zM2 9h2v6H2zm0 8h2v3h3v2H4a2 2 0 0 1-2-2zm7 5v-2h6v2zm6-20v2H9V2zM6 17h12l-4-5-3 3.8-2-2.6z"/><path d="M2 7h2V4h3V2H4a2 2 0 0 0-2 2zm20 0h-2V4h-3V2h3a2 2 0 0 1 2 2zm0 2h-2v6h2zm0 8h-2v3h-3v2h3a2 2 0 0 0 2-2zM2 9h2v6H2zm0 8h2v3h3v2H4a2 2 0 0 1-2-2zm7 5v-2h6v2zm6-20v2H9V2zM6 17h12l-4-5-3 3.8-2-2.6z"/><path d="M6 17h12l-4-5-3 3.8-2-2.6z"/><path d="M6 17h12l-4-5-3 3.8-2-2.6z"/><path d="M6 17h12l-4-5-3 3.8-2-2.6z"/><path d="M6 17h12l-4-5-3 3.8-2-2.6z"/></svg>',
'addtag': '<svg width="24" height="24"><path fill-rule="evenodd" clip-rule="evenodd" d="M15 5a2 2 0 0 1 1.6.8L21 12l-4.4 6.2a2 2 0 0 1-1.6.8h-3v-2h3l3.5-5L15 7H5v3H3V7c0-1.1.9-2 2-2h10Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M6 12a1 1 0 0 0-1 1v2H3a1 1 0 1 0 0 2h2v2a1 1 0 1 0 2 0v-2h2a1 1 0 1 0 0-2H7v-2c0-.6-.4-1-1-1Z"/></svg>',
'adjust-length': '<svg width="24" height="24"><path d="M14 7c0 .9.5 1.8 1.5 2.2h.2c.5.3.9.7 1.1 1.3v.1l.1.1v.1h.1V14h-2V9h-2v5h-2V9H9v5H7V9H4v9h16v-6.2a2.3 2.3 0 0 0 1-1l.1-.1v-.2h.1c.2-.5.4-.8.8-1V18c0 .6-.2 1-.6 1.4a2 2 0 0 1-1.4.6H4a2 2 0 0 1-1.4-.6A2 2 0 0 1 2 18V9c0-.6.2-1 .6-1.4A2 2 0 0 1 4 7h10Z"/><path d="M18.7 4.2a.3.3 0 0 1 .6 0 4.1 4.1 0 0 0 2.5 2.5.3.3 0 0 1 0 .6l-.2.1a4.1 4.1 0 0 0-2.3 2.4c-.1.3-.5.3-.6 0a4.1 4.1 0 0 0-.6-1 4.2 4.2 0 0 0-1.7-1.4h-.2a.3.3 0 0 1 0-.7 4.1 4.1 0 0 0 2.5-2.5Z"/></svg>',
'adjustments': '<svg width="24" height="24"><path d="M16 11a3 3 0 1 1 2.8-4H21v2h-2.2a3 3 0 0 1-2.8 2Zm0-2a1 1 0 1 1 0-2 1 1 0 0 1 0 2ZM3 9h8V7H3v2Zm5 10a3 3 0 1 0-2.8-4H3v2h2.2A3 3 0 0 0 8 19Zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm13 0h-8v-2h8v2Z"/></svg>',
'ai-assistant': '<svg height="24" width="24"><path d="m15.4 2.6-1.9 1.9h-4a5.5 5.5 0 1 0 0 11H13V18c2.6-2 6.8-4 7-7.6V10l1.8-1.8.2 1.8c0 5.7-7 9-11 12v-4.5H9.5a7.5 7.5 0 0 1 0-15h6z"/><path d="M8.5 8.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm7 0a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm3.2-6.3a.3.3 0 0 1 .6 0 4.1 4.1 0 0 0 2.5 2.5c.3 0 .3.5 0 .6l-.2.1a4.1 4.1 0 0 0-2.3 2.4c-.1.3-.5.3-.6 0a4.1 4.1 0 0 0-2.3-2.4h-.2a.3.3 0 0 1 0-.7 4.1 4.1 0 0 0 2.5-2.5z"/></svg>',
'ai-chat-response': '<svg width="24" height="24"><path d="M9 9a7.1 7.1 0 0 0 6 6 7.1 7.1 0 0 0-6 6 7.1 7.1 0 0 0-6-6 7.1 7.1 0 0 0 6-6Zm7.5-5A4.6 4.6 0 0 0 21 8.5a4.6 4.6 0 0 0-4.5 4.5A4.6 4.6 0 0 0 12 8.5 4.6 4.6 0 0 0 16.5 4ZM6 3a5 5 0 0 0 3 3 5 5 0 0 0-3 3 5 5 0 0 0-3-3 5 5 0 0 0 3-3Z"/></svg>',
'ai-model': '<svg height="24" width="24"><path d="M12.1 3c.5 0 .9.5.9 1v3h5a2 2 0 0 1 2 2v4h2v2h-2v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-4H2v-2h2V9c0-1.1.9-2 2-2h5V5H7.4V3H12zM6 19h12V9H6zm3-7c.6 0 1 .4 1 1v2a1 1 0 1 1-2 0v-2c0-.6.4-1 1-1zm6 0c.6 0 1 .4 1 1v2a1 1 0 1 1-2 0v-2c0-.6.4-1 1-1z"/></svg>',
'ai-prompt': '<svg width="24" height="24"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M15 6.7a1 1 0 0 0-1.4 0l-9.9 10a1 1 0 0 0 0 1.3l2.1 2.1c.4.4 1 .4 1.4 0l10-9.9c.3-.3.3-1 0-1.4l-2.2-2Zm1.4 2.8-2-2-3 2.7 2.2 2.2 2.8-2.9Z"/><path d="m18.5 7.3-.7-1.5-1.5-.8 1.5-.7.7-1.5.7 1.5 1.5.7-1.5.8-.7 1.5ZM18.5 16.5l-.7-1.6-1.5-.7 1.5-.7.7-1.6.7 1.6 1.5.7-1.5.7-.7 1.6ZM9.7 7.3 9 5.8 7.5 5 9 4.3l.7-1.5.7 1.5L12 5l-1.5.8-.7 1.5Z"/></g><defs><clipPath id="a"><path d="M0 0h24v24H0z"/></clipPath></defs></svg>',
'ai-review': '<svg height="24" width="24"><path d="M15.4 12.6zM15.3 12.6h.1l.3-.2a.5.5 0 0 0 0-.8 5.5 5.5 0 0 1-3.3-3.3.5.5 0 0 0-.8 0 5.5 5.5 0 0 1-3.3 3.3.5.5 0 0 0 0 .8l.3.1a5.5 5.5 0 0 1 2.7 2.6l.1.2.2.4a.5.5 0 0 0 .8 0l.1-.2v-.2l.2-.2a5.5 5.5 0 0 1 2.6-2.5zM12 11a7.3 7.3 0 0 1-1 .9l1 1a7.3 7.3 0 0 1 1-1 7.3 7.3 0 0 1-1-1z" fill-rule="evenodd"/><path d="M12 4a10 10 0 0 1 1.5.1l-.3 2A7.6 7.6 0 0 0 12 6c-1.7 0-3.2.5-4.5 1.5-1.4 1-2.6 2.4-3.4 4a1 1 0 0 0-.1.5 1 1 0 0 0 0 .3l.1.1c.8 1.7 2 3 3.4 4.1a7.5 7.5 0 0 0 5.3 1.5c1.4-.2 2.6-.7 3.7-1.5 1.2-.9 2.2-2 3-3.4a13.5 13.5 0 0 0 .4-.7 1 1 0 0 0 .1-.4 1 1 0 0 0-.1-.4 12.5 12.5 0 0 0-.7-1.3l1.6-1.1a13.7 13.7 0 0 1 .9 1.5 3 3 0 0 1 .2 2 3 3 0 0 1-.2.6 15.1 15.1 0 0 1-.4.8c-1 1.6-2.1 3-3.6 4-1.4 1-3 1.7-4.7 1.8a9.9 9.9 0 0 1-1 .1c-2 0-4-.6-5.7-1.9-1.7-1.2-3-2.8-4-4.8a3 3 0 0 1 0-2.6c1-2 2.3-3.6 4-4.8A9.5 9.5 0 0 1 12 4z"/><path d="M16.7 4.2a.3.3 0 0 1 .6 0 4.1 4.1 0 0 0 2.5 2.5.3.3 0 0 1 0 .6l-.2.1a4.1 4.1 0 0 0-2.3 2.4c-.1.3-.5.3-.6 0a4.1 4.1 0 0 0-2.3-2.4h-.2a.3.3 0 0 1 0-.7 4.1 4.1 0 0 0 2.5-2.5z"/></svg>',
'ai-translate': '<svg height="24" width="24"><path d="M22 22h-2.1l-1-3h-5l-1 3h-2.1l4.7-12h2zm-7.4-4.8h3.6l-1.8-5.1z" fill-rule="evenodd"/><path d="M8.9 4h5.2s-.8 2.5-2.7 5.7c-.7 1.2-2 2.9-2 2.9l2.4 2.5-1 1.8-2.9-3-4.5 4.6L2 17.1l4.6-4.5-1.6-2C4.6 9.9 4 9 3.7 8h2A15.3 15.3 0 0 0 8 11.1l1.7-2.3c.7-1 1-1.9 1.3-2.8H2V4h4.9V2h2zm9.3-1.8a.3.3 0 0 1 .6 0 4.1 4.1 0 0 0 2.5 2.5.3.3 0 0 1 0 .6l-.2.1a4.1 4.1 0 0 0-2.3 2.4c-.1.3-.5.3-.6 0a4.1 4.1 0 0 0-2.3-2.4h-.2a.3.3 0 0 1 0-.7 4.1 4.1 0 0 0 2.5-2.5z"/></svg>',
'ai': '<svg width="24" height="24"><path fill-rule="evenodd" clip-rule="evenodd" d="M5 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3H5Zm6.8 11.5.5 1.2a68.3 68.3 0 0 0 .7 1.1l.4.1c.3 0 .5 0 .7-.3.2-.1.3-.3.3-.6l-.3-1-2.6-6.2a20.4 20.4 0 0 0-.5-1.3l-.5-.4-.7-.2c-.2 0-.5 0-.6.2-.2 0-.4.2-.5.4l-.3.6-.3.7L5.7 15l-.2.6-.1.4c0 .3 0 .5.3.7l.6.2c.3 0 .5 0 .7-.2l.4-1 .5-1.2h3.9ZM9.8 9l1.5 4h-3l1.5-4Zm5.6-.9v7.6c0 .4 0 .7.2 1l.7.2c.3 0 .6 0 .8-.3l.2-.9V8.1c0-.4 0-.7-.2-.9a1 1 0 0 0-.8-.3c-.2 0-.5.1-.7.3l-.2 1Z"/></svg>',
'align-center': '<svg width="24" height="24"><path d="M5 5h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm3 4h8c.6 0 1 .4 1 1s-.4 1-1 1H8a1 1 0 1 1 0-2Zm0 8h8c.6 0 1 .4 1 1s-.4 1-1 1H8a1 1 0 0 1 0-2Zm-3-4h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Z" fill-rule="evenodd"/></svg>',
'align-justify': '<svg width="24" height="24"><path d="M5 5h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm0 4h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm0 4h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Zm0 4h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Z" fill-rule="evenodd"/></svg>',
'align-left': '<svg width="24" height="24"><path d="M5 5h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm0 4h8c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm0 8h8c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Zm0-4h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Z" fill-rule="evenodd"/></svg>',
'align-none': '<svg width="24" height="24"><path d="M14.2 5 13 7H5a1 1 0 1 1 0-2h9.2Zm4 0h.8a1 1 0 0 1 0 2h-2l1.2-2Zm-6.4 4-1.2 2H5a1 1 0 0 1 0-2h6.8Zm4 0H19a1 1 0 0 1 0 2h-4.4l1.2-2Zm-6.4 4-1.2 2H5a1 1 0 0 1 0-2h4.4Zm4 0H19a1 1 0 0 1 0 2h-6.8l1.2-2ZM7 17l-1.2 2H5a1 1 0 0 1 0-2h2Zm4 0h8a1 1 0 0 1 0 2H9.8l1.2-2Zm5.2-13.5 1.3.7-9.7 16.3-1.3-.7 9.7-16.3Z" fill-rule="evenodd"/></svg>',
'align-right': '<svg width="24" height="24"><path d="M5 5h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm6 4h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm0 8h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm-6-4h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Z" fill-rule="evenodd"/></svg>',
'alt-text': '<svg width="24" height="24"><path d="M3 20a2 2 0 0 1-1.4-.6A2 2 0 0 1 1 18V6c0-.6.2-1 .6-1.4A2 2 0 0 1 3 4h18c.6 0 1 .2 1.4.6.4.4.6.9.6 1.4v12c0 .6-.2 1-.6 1.4a2 2 0 0 1-1.4.6H3Zm0-2h18V6H3v12Zm1.5-3H6v-1.5h1.5V15H9v-5a1 1 0 0 0-.3-.7A1 1 0 0 0 8 9H5.5a1 1 0 0 0-.7.3 1 1 0 0 0-.3.7v5ZM6 12v-1.5h1.5V12H6Z"/><path d="M11 15V9h1.3v4.5h2V15H11Zm5.8-4.5V15h1.4v-4.5h1.3V9h-4v1.5h1.3Z"/></svg>',
'arrow-left': '<svg width="24" height="24"><path d="m5.6 13 12 6a1 1 0 0 0 1.4-1V6a1 1 0 0 0-1.4-.9l-12 6a1 1 0 0 0 0 1.8Z" fill-rule="evenodd"/></svg>',
'arrow-right': '<svg width="24" height="24"><path d="m18.5 13-12 6A1 1 0 0 1 5 18V6a1 1 0 0 1 1.4-.9l12 6a1 1 0 0 1 0 1.8Z" fill-rule="evenodd"/></svg>',
'attachment': '<svg height="24" width="24"><path d="M11 2c1 0 2 .5 2.8 1.3.7.8 1.1 1.9 1.1 3L15 16c0 .7-.2 1.3-.7 1.8-.4.5-1 .8-1.8.8-.6 0-1.3-.3-1.7-.8-.5-.5-.7-1.1-.7-1.8L10 6.3h2V16l.2.5.3.1.4-.1.1-.5V6.3c0-.7-.3-1.3-.7-1.7-.3-.4-.8-.6-1.3-.6s-1 .2-1.4.6c-.4.4-.6 1-.6 1.7V16c0 1 .5 2 1.1 2.8.7.7 1.6 1.1 2.4 1.1 1 0 1.8-.4 2.5-1.1.6-.7 1-1.7 1-2.8l-.1-9h2l.1 9c0 1.5-.6 3-1.6 4.1a5.3 5.3 0 0 1-3.9 1.8c-1.4 0-2.8-.6-3.8-1.8A6.2 6.2 0 0 1 7 16.1L7 6.3V6c0-1 .4-2 1.1-2.8C9 2.5 10 2 11 2z"/></svg>',
'auto-image-enhancement': '<svg width="24" height="24"><path d="M7.7 10.6a5.8 5.8 0 0 0 5.7 5.7A5.8 5.8 0 0 0 7.7 22 5.8 5.8 0 0 0 2 16.3a5.8 5.8 0 0 0 5.7-5.7ZM15.5 2c.3 3 3.3 6 6.5 6.4-3.2.5-6.2 3.4-6.5 6.5-.3-3.4-3-6-6.4-6.5 3.3-.5 6.1-3 6.4-6.4Z"/></svg>',
'blur': '<svg width="24" height="24"><path d="M19.3 9.3a1 1 0 0 0-.3.7c0 .3.1.5.3.7.2.2.4.3.7.3s.5-.1.7-.3a1 1 0 0 0 .3-.7 1 1 0 0 0-.3-.7A1 1 0 0 0 20 9a1 1 0 0 0-.7.3Zm-16 0a1 1 0 0 0-.3.7c0 .3.1.5.3.7.2.2.4.3.7.3s.5-.1.7-.3A1 1 0 0 0 5 10a1 1 0 0 0-.3-.7A1 1 0 0 0 4 9a1 1 0 0 0-.7.3Zm16 6a1 1 0 0 0-.3.7c0 .3.1.5.3.7.2.2.4.3.7.3s.5-.1.7-.3a1 1 0 0 0 .3-.7 1 1 0 0 0-.3-.7 1 1 0 0 0-.7-.3 1 1 0 0 0-.7.3Zm-16 0a1 1 0 0 0-.3.7c0 .3.1.5.3.7.2.2.4.3.7.3s.5-.1.7-.3A1 1 0 0 0 5 16a1 1 0 0 0-.3-.7A1 1 0 0 0 4 15a1 1 0 0 0-.7.3Zm5-11A1 1 0 0 0 8 5a1 1 0 0 0 1 1c.3 0 .5-.1.7-.3A1 1 0 0 0 10 5a1 1 0 0 0-.3-.7A1 1 0 0 0 9 4a1 1 0 0 0-.7.3Zm6 0a1 1 0 0 0-.3.7 1 1 0 0 0 1 1c.3 0 .5-.1.7-.3A1 1 0 0 0 16 5a1 1 0 0 0-.3-.7A1 1 0 0 0 15 4a1 1 0 0 0-.7.3Zm-6 16a1 1 0 0 0-.3.7c0 .3.1.5.3.7.2.2.4.3.7.3s.5-.1.7-.3a1 1 0 0 0 .3-.7 1 1 0 0 0-.3-.7A1 1 0 0 0 9 20a1 1 0 0 0-.7.3Zm6 0a1 1 0 0 0-.3.7c0 .3.1.5.3.7.2.2.4.3.7.3s.5-.1.7-.3a1 1 0 0 0 .3-.7 1 1 0 0 0-.3-.7 1 1 0 0 0-.7-.3 1 1 0 0 0-.7.3ZM7.6 8.6A2 2 0 0 0 7 10c0 .6.2 1 .6 1.4.4.4.8.6 1.4.6s1-.2 1.4-.6c.4-.4.6-.8.6-1.4s-.2-1-.6-1.4A2 2 0 0 0 9 8a2 2 0 0 0-1.4.6Zm0 6A2 2 0 0 0 7 16c0 .6.2 1 .6 1.4.4.4.8.6 1.4.6s1-.2 1.4-.6c.4-.4.6-.8.6-1.4s-.2-1-.6-1.4A2 2 0 0 0 9 14a2 2 0 0 0-1.4.6Zm6-6A2 2 0 0 0 13 10c0 .6.2 1 .6 1.4.4.4.8.6 1.4.6s1-.2 1.4-.6c.4-.4.6-.8.6-1.4s-.2-1-.6-1.4A2 2 0 0 0 15 8a2 2 0 0 0-1.4.6Zm0 6A2 2 0 0 0 13 16c0 .6.2 1 .6 1.4.4.4.8.6 1.4.6s1-.2 1.4-.6c.4-.4.6-.8.6-1.4s-.2-1-.6-1.4A2 2 0 0 0 15 14a2 2 0 0 0-1.4.6Z"/></svg>',
'bold': '<svg width="24" height="24"><path d="M7.8 19c-.3 0-.5 0-.6-.2l-.2-.5V5.7c0-.2 0-.4.2-.5l.6-.2h5c1.5 0 2.7.3 3.5 1 .7.6 1.1 1.4 1.1 2.5a3 3 0 0 1-.6 1.9c-.4.6-1 1-1.6 1.2.4.1.9.3 1.3.6s.8.7 1 1.2c.4.4.5 1 .5 1.6 0 1.3-.4 2.3-1.3 3-.8.7-2.1 1-3.8 1H7.8Zm5-8.3c.6 0 1.2-.1 1.6-.5.4-.3.6-.7.6-1.3 0-1.1-.8-1.7-2.3-1.7H9.3v3.5h3.4Zm.5 6c.7 0 1.3-.1 1.7-.4.4-.4.6-.9.6-1.5s-.2-1-.7-1.4c-.4-.3-1-.4-2-.4H9.4v3.8h4Z" fill-rule="evenodd"/></svg>',
'bookmark': '<svg width="24" height="24"><path d="M6 4v17l6-4 6 4V4c0-.6-.4-1-1-1H7a1 1 0 0 0-1 1Z" fill-rule="nonzero"/></svg>',
'border-style': '<svg width="24" height="24"><g fill-rule="evenodd"><rect width="18" height="2" x="3" y="6" rx="1"/><rect width="2.8" height="2" x="3" y="16" rx="1"/><rect width="2.8" height="2" x="6.8" y="16" rx="1"/><rect width="2.8" height="2" x="10.6" y="16" rx="1"/><rect width="2.8" height="2" x="14.4" y="16" rx="1"/><rect width="2.8" height="2" x="18.2" y="16" rx="1"/><rect width="8" height="2" x="3" y="11" rx="1"/><rect width="8" height="2" x="13" y="11" rx="1"/></g></svg>',
'border-width': '<svg width="24" height="24"><g fill-rule="evenodd"><rect width="18" height="5" x="3" y="5" rx="1"/><rect width="18" height="3.5" x="3" y="11.5" rx="1"/><rect width="18" height="2" x="3" y="17" rx="1"/></g></svg>',
'box': '<svg width="24" height="24"><path d="M3.8 7a1 1 0 0 0-.7.6l-.1 3V13.8a3.5 3.5 0 0 0 4 2.7c.8-.2 1.4-.5 2-1l.3-.3.3.2c.7.7 1.4 1 2.4 1.1 1.2 0 2.4-.6 3-1.6.9-1.2.8-2.8 0-4a3.7 3.7 0 0 0-2-1.2c-.4-.1-1.2-.1-1.6 0a3.4 3.4 0 0 0-1.7 1l-.3.2-.2-.1c-.4-.5-1.2-1-2-1.1-.6-.2-1.5 0-2 .2H5V9c0-1.3 0-1.3-.3-1.6a1 1 0 0 0-1-.3Zm12 2.6c-.3.2-.5.4-.6.8v.6l.9 1 .7 1v.1l-.8 1-.7 1a1 1 0 0 0 0 .7 1 1 0 0 0 1.3.6c.2 0 .3-.2 1-1l.5-.7.6.8.7.8c.4.3 1 .3 1.3 0l.2-.3a1 1 0 0 0 0-.8l-.8-1.2-.7-1a26 26 0 0 0 1.5-2 .8.8 0 0 0 .1-.4c0-.4-.1-.7-.5-.9H20c-.4 0-.5 0-1.2.8l-.7.8-.6-.7-.7-.9a1 1 0 0 0-1 0Zm-9 2 .4.1c.3.1.6.5.7.8l.1.6v.6c-.6 1-2 1.2-2.7.3-.3-.3-.3-.5-.3-1v-.5c.3-.5.7-.8 1.1-1a1.5 1.5 0 0 1 .7 0Zm5.7 0c.4 0 .8.4 1 .8l.2.6-.1.7c-.5.9-1.6 1.1-2.4.6a1.4 1.4 0 0 1-.6-1.1 1.4 1.4 0 0 1 .5-1.2 1.5 1.5 0 0 1 1.4-.4Z"/></svg>',
'brightness': '<svg width="24" height="24"><path d="M12.7 18.3a1 1 0 0 0-.7-.3 1 1 0 0 0-.7.3 1 1 0 0 0-.3.7v1a1 1 0 0 0 .3.7 1 1 0 0 0 .7.3 1 1 0 0 0 .7-.3 1 1 0 0 0 .3-.7v-1a1 1 0 0 0-.3-.7ZM11.3 5.7a1 1 0 0 0 .7.3 1 1 0 0 0 .7-.3A1 1 0 0 0 13 5V4a1 1 0 0 0-.3-.7A1 1 0 0 0 12 3a1 1 0 0 0-.7.3 1 1 0 0 0-.3.7v1a1 1 0 0 0 .3.7Zm9.4 5.6a1 1 0 0 0-.7-.3h-1a1 1 0 0 0-.7.3 1 1 0 0 0-.3.7 1 1 0 0 0 .3.7 1 1 0 0 0 .7.3h1a1 1 0 0 0 .7-.3 1 1 0 0 0 .3-.7 1 1 0 0 0-.3-.7Zm-15 1.4A1 1 0 0 0 6 12a1 1 0 0 0-.3-.7A1 1 0 0 0 5 11H4a1 1 0 0 0-.7.3 1 1 0 0 0-.3.7 1 1 0 0 0 .3.7 1 1 0 0 0 .7.3h1a1 1 0 0 0 .7-.3Zm13 4.6-.7-.8a1 1 0 0 0-.8-.3 1 1 0 0 0-.7.3 1 1 0 0 0-.3.7 1 1 0 0 0 .3.8l.7.7a1 1 0 0 0 .7.3 1 1 0 0 0 .8-.3 1 1 0 0 0 .3-.8 1 1 0 0 0-.3-.7ZM5.3 6.7l.7.8a1 1 0 0 0 .8.3 1 1 0 0 0 .7-.3 1 1 0 0 0 .3-.7 1 1 0 0 0-.3-.8l-.7-.7A1 1 0 0 0 6 5a1 1 0 0 0-.8.3 1 1 0 0 0-.3.8 1 1 0 0 0 .3.6Zm12 1a1 1 0 0 0 .7-.2l.7-.7A1 1 0 0 0 19 6a1 1 0 0 0-.3-.8 1 1 0 0 0-.8-.3 1 1 0 0 0-.7.3l-.7.7a1 1 0 0 0-.3.8 1 1 0 0 0 .3.7 1 1 0 0 0 .7.2ZM6.7 16.4a1 1 0 0 0-.8.2l-.7.7a1 1 0 0 0-.3.7 1 1 0 0 0 .3.8 1 1 0 0 0 .8.3 1 1 0 0 0 .7-.3l.7-.7a1 1 0 0 0 .3-.8 1 1 0 0 0-.3-.7 1 1 0 0 0-.7-.3Z"/><path fill-rule="evenodd" d="M13.6 8.3A4 4 0 0 0 12 8a4 4 0 0 0-1.6.3A4 4 0 0 0 9 9.5a4 4 0 0 0-.6 1A4 4 0 0 0 8 12c0 .6.1 1 .3 1.6a4 4 0 0 0 2.1 2 4 4 0 0 0 1.6.4 4 4 0 0 0 1.6-.3 4 4 0 0 0 2-2.1A4 4 0 0 0 16 12a4 4 0 0 0-.3-1.6 4 4 0 0 0-2.1-2ZM14 12a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z"/></svg>',
'browse': '<svg width="24" height="24"><path d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4v-2h4V8H5v10h4v2H5a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h14Zm-8 9.4-2.3 2.3a1 1 0 1 1-1.4-1.4l4-4a1 1 0 0 1 1.4 0l4 4a1 1 0 0 1-1.4 1.4L13 13.4V20a1 1 0 0 1-2 0v-6.6Z" fill-rule="nonzero"/></svg>',
'camera': '<svg height="24" width="24"><g clip-rule="evenodd" fill-rule="evenodd"><path d="M10 5.4a1 1 0 0 1 .8-.4h4.4a1 1 0 0 1 .8.4l1.2 1.8h2.3A2.5 2.5 0 0 1 22 9.6v8a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 4 17.6v-8a2.5 2.5 0 0 1 2.5-2.4h2.3zM11.4 7l-1.2 1.7a1 1 0 0 1-.8.5h-3a.5.5 0 0 0-.4.4v8a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.4h-2.9a1 1 0 0 1-.8-.5L14.6 7z"/><path d="M13 11.4a2 2 0 1 0 0 3.8 2 2 0 0 0 0-3.8zm-4 1.9a4 4 0 1 1 8 0 4 4 0 0 1-8 0z"/></g></svg>',
'cancel': '<svg width="24" height="24"><path d="M12 4.6a7.4 7.4 0 1 1 0 14.8 7.4 7.4 0 0 1 0-14.8ZM12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 8L14.8 8l1 1.1-2.7 2.8 2.7 2.7-1.1 1.1-2.7-2.7-2.7 2.7-1-1.1 2.6-2.7-2.7-2.7 1-1.1 2.8 2.7Z" fill-rule="nonzero"/></svg>',
'caption': '<svg height="24" width="24"><path d="M6 16h8v-2H6zm10 0h2v-2h-2zM4 20a2 2 0 0 1-1.5-.6A2 2 0 0 1 2 18V6c0-.6.2-1 .6-1.4A2 2 0 0 1 4 4h16c.6 0 1 .2 1.4.6.4.4.6.9.6 1.4v12c0 .6-.2 1-.6 1.4a2 2 0 0 1-1.4.6zm0-2h16V6H4z"/></svg>',
'cell-background-color': '<svg width="24" height="24"><path d="m15.7 2 1.6 1.6-2.7 2.6 5.9 5.8c.7.7.7 1.7 0 2.4l-6.3 6.1a1.7 1.7 0 0 1-2.4 0l-6.3-6.1c-.7-.7-.7-1.7 0-2.4L15.7 2ZM18 12l-4.5-4L9 12h9ZM4 16s2 2.4 2 3.8C6 21 5.1 22 4 22s-2-1-2-2.2C2 18.4 4 16 4 16Z"/></svg>',
'cell-border-color': '<svg width="24" height="24"><g fill-rule="evenodd"><path fill-rule="nonzero" d="M5 13v5h2v2H5a2 2 0 0 1-2-2v-5h2zm8-7V4h6a2 2 0 0 1 2 2h-8z" opacity=".2"/><path fill-rule="nonzero" d="M13 4v2H5v7H3V6c0-1.1.9-2 2-2h8zm-2.6 14.1.1-.1.1.1.2.3.2.2.2.2c.4.6.8 1.2.8 1.7 0 .8-.7 1.5-1.5 1.5S9 21.3 9 20.5c0-.5.4-1.1.8-1.7l.2-.2.2-.2.2-.3z"/><path d="m13 11-2 2H5v-2h6V6h2z"/><path fill-rule="nonzero" d="m18.4 8 1 1-1.8 1.9 4 4c.5.4.5 1.1 0 1.6l-4.3 4.2a1.2 1.2 0 0 1-1.6 0l-4.4-4.2c-.4-.5-.4-1.2 0-1.7l7-6.8Zm1.6 7-3-3-3 3h6Z"/></g></svg>',
'change-case': '<svg width="24" height="24"><path d="M18.4 18.2v-.6c-.5.8-1.3 1.2-2.4 1.2-2.2 0-3.3-1.6-3.3-4.8 0-3.1 1-4.7 3.3-4.7 1.1 0 1.8.3 2.4 1.1v-.6c0-.5.4-.8.8-.8s.8.3.8.8v8.4c0 .5-.4.8-.8.8a.8.8 0 0 1-.8-.8zm-2-7.4c-1.3 0-1.8.9-1.8 3.2 0 2.4.5 3.3 1.7 3.3 1.3 0 1.8-.9 1.8-3.2 0-2.4-.5-3.3-1.7-3.3zM10 15.7H5.5l-.8 2.6a1 1 0 0 1-1 .7h-.2a.7.7 0 0 1-.7-1l4-12a1 1 0 0 1 2 0l4 12a.7.7 0 0 1-.8 1h-.2a1 1 0 0 1-1-.7l-.8-2.6zm-.3-1.5-2-6.5-1.9 6.5h3.9z" fill-rule="evenodd"/></svg>',
'change-tone': '<svg height="24" width="24"><path d="M13 20h-2V8h2zm-4-3H7v-6h2zm8 0h-2v-6h2zM5 15.5H3v-3h2zm16 0h-2v-3h2zM17.7 4.2a.3.3 0 0 1 .6 0 4.1 4.1 0 0 0 2.5 2.5.3.3 0 0 1 0 .6l-.2.1a4.1 4.1 0 0 0-2.3 2.4c-.1.3-.5.3-.6 0a4.1 4.1 0 0 0-2.3-2.4h-.2a.3.3 0 0 1 0-.7 4.1 4.1 0 0 0 2.5-2.5z"/></svg>',
'character-count': '<svg width="24" height="24"><path d="M4 11.5h16v1H4v-1Zm4.8-6.8V10H7.7V5.8h-1v-1h2ZM11 8.3V9h2v1h-3V7.7l2-1v-.9h-2v-1h3v2.4l-2 1Zm6.3-3.4V10h-3.1V9h2.1V8h-2.1V6.8h2.1v-1h-2.1v-1h3.1ZM5.8 16.4c0-.5.2-.8.5-1 .2-.2.6-.3 1.2-.3l.8.1c.2 0 .4.2.5.3l.4.4v2.8l.2.3H8.2V18.7l-.6.3H7c-.4 0-.7 0-1-.2a1 1 0 0 1-.3-.9c0-.3 0-.6.3-.8.3-.2.7-.4 1.2-.4l.6-.2h.3v-.2l-.1-.2a.8.8 0 0 0-.5-.1 1 1 0 0 0-.4 0l-.3.4h-1Zm2.3.8h-.2l-.2.1-.4.1a1 1 0 0 0-.4.2l-.2.2.1.3.5.1h.4l.4-.4v-.6Zm2-3.4h1.2v1.7l.5-.3h.5c.5 0 .9.1 1.2.5.3.4.5.8.5 1.4 0 .6-.2 1.1-.5 1.5-.3.4-.7.6-1.3.6l-.6-.1-.4-.4v.4h-1.1v-5.4Zm1.1 3.3c0 .3 0 .6.2.8a.7.7 0 0 0 1.2 0l.2-.8c0-.4 0-.6-.2-.8a.7.7 0 0 0-.6-.3l-.6.3-.2.8Zm6.1-.5c0-.2 0-.3-.2-.4a.8.8 0 0 0-.5-.2c-.3 0-.5.1-.6.3l-.2.9c0 .3 0 .6.2.8.1.2.3.3.6.3.2 0 .4 0 .5-.2l.2-.4h1.1c0 .5-.3.8-.6 1.1a2 2 0 0 1-1.3.4c-.5 0-1-.2-1.3-.6a2 2 0 0 1-.5-1.4c0-.6.1-1.1.5-1.5.3-.4.8-.5 1.4-.5.5 0 1 0 1.2.3.4.3.5.7.5 1.2h-1v-.1Z" fill-rule="evenodd"/></svg>',
'chat-commands': '<svg height="24" width="24"><path d="M14.3 3.3h.4l-2 2H9.3a5.5 5.5 0 0 0 0 11h3.4v2.1l4.4-3h.1a5.5 5.5 0 0 0 2.5-4.3v-.8l1.7-1.7a7.5 7.5 0 0 1-3.1 8.5l-7.6 5.1v-4H9.3a7.5 7.5 0 0 1 0-15z"/><path d="M12.4 7.3a1 1 0 0 1 1.5 1.4l-.8.8a1 1 0 0 1 .8 1.7l-2.3 2.5a1 1 0 0 1-1.5-1.4l.8-.8a1 1 0 0 1-.8-1.7zm6.3-5a.3.3 0 0 1 .6 0 4.1 4.1 0 0 0 2.5 2.4.3.3 0 0 1 0 .6l-.2.1a4.1 4.1 0 0 0-2.3 2.4c-.1.3-.5.3-.6 0a4.1 4.1 0 0 0-2.3-2.4h-.2a.3.3 0 0 1 0-.7 4.1 4.1 0 0 0 2.5-2.5z"/></svg>',
'chat-reasoning': '<svg width="16" height="16"><path d="M9.1 12.7V14H7v-1.3H9Zm.6-2.2V12H6.3v-1.4h3.4Zm1-4.6A2.5 2.5 0 0 0 9.9 4 2.7 2.7 0 0 0 8 3.3c-.7 0-1.4.3-1.9.8a2.5 2.5 0 0 0-.8 1.8c0 .4.1.8.7 1.4.4.4.8.9 1 1.6a.7.7 0 1 1-1.3.3 2 2 0 0 0-.7-1A3 3 0 0 1 4 6c0-1 .4-2 1.2-2.8A4 4 0 0 1 8 2c1 0 2 .4 2.8 1.1A3.8 3.8 0 0 1 12 6a3.1 3.1 0 0 1-1 2.3l-.7 1A.7.7 0 1 1 9 8.9c.2-.7.6-1.2 1-1.6a1.8 1.8 0 0 0 .7-1.4Z"/></svg>',
'chat-send': '<svg width="24" height="24"><path d="M11 19a1 1 0 1 0 2 0V7.8l5.3 5.3a1 1 0 1 0 1.4-1.4L12 4l-7.7 7.7a1 1 0 1 0 1.4 1.4L11 7.8V19Z"/></svg>',
'chat-web-search': '<svg height="16" width="16"><path d="M8 2.5a6 6 0 1 1 0 12 6 6 0 0 1 0-12zM3.4 9.2a4.7 4.7 0 0 0 3.1 3.7v-.2a8.8 8.8 0 0 1-1.1-3.5zm7.2 0a8.8 8.8 0 0 1-1 3.5l-.1.2a4.7 4.7 0 0 0 3.1-3.7zm-3.9 0A7.5 7.5 0 0 0 8 12.8a7.5 7.5 0 0 0 1.3-3.6zM6.5 4a4.7 4.7 0 0 0-3.1 3.7h2a8.8 8.8 0 0 1 1-3.5l.1-.2zm1.5.1a7.5 7.5 0 0 0-1.3 3.6h2.6A7.5 7.5 0 0 0 8 4.2zm1.6 0a8.8 8.8 0 0 1 1 3.6h2a4.7 4.7 0 0 0-3.1-3.7v.2z" fill-rule="evenodd"/></svg>',
'checklist-rtl': '<svg width="24" height="24"><path d="M5 17h8c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2zm0-6h8c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2zm0-6h8c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2zm14.2 11c.2-.4.6-.5.9-.3.3.2.4.6.2 1L18 20c-.2.3-.7.4-1 0l-1.3-1.3a.7.7 0 0 1 0-1c.3-.2.7-.2 1 0l.7.9 1.7-2.8zm0-6c.2-.4.6-.5.9-.3.3.2.4.6.2 1L18 14c-.2.3-.7.4-1 0l-1.3-1.3a.7.7 0 0 1 0-1c.3-.2.7-.2 1 0l.7.9 1.7-2.8zm0-6c.2-.4.6-.5.9-.3.3.2.4.6.2 1L18 8c-.2.3-.7.4-1 0l-1.3-1.3a.7.7 0 0 1 0-1c.3-.2.7-.2 1 0l.7.9 1.7-2.8z" fill-rule="evenodd"/></svg>',
'checklist': '<svg width="24" height="24"><path d="M11 17h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm0-6h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm0-6h8a1 1 0 0 1 0 2h-8a1 1 0 0 1 0-2ZM7.2 16c.2-.4.6-.5.9-.3.3.2.4.6.2 1L6 20c-.2.3-.7.4-1 0l-1.3-1.3a.7.7 0 0 1 0-1c.3-.2.7-.2 1 0l.7.9 1.7-2.8Zm0-6c.2-.4.6-.5.9-.3.3.2.4.6.2 1L6 14c-.2.3-.7.4-1 0l-1.3-1.3a.7.7 0 0 1 0-1c.3-.2.7-.2 1 0l.7.9 1.7-2.8Zm0-6c.2-.4.6-.5.9-.3.3.2.4.6.2 1L6 8c-.2.3-.7.4-1 0L3.8 6.9a.7.7 0 0 1 0-1c.3-.2.7-.2 1 0l.7.9 1.7-2.8Z" fill-rule="evenodd"/></svg>',
'checkmark-filled': '<svg width="24" height="24"><path fill-rule="evenodd" d="M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20Zm4.8 5a1 1 0 0 0-1.4.3l-5.4 8-2-2.1a1 1 0 1 0-1.5 1.4l3 3a1 1 0 0 0 1.5-.2l6-9a1 1 0 0 0-.2-1.4Z"/></svg>',
'checkmark': '<svg width="24" height="24"><path d="M18.2 5.4a1 1 0 0 1 1.6 1.2l-8 12a1 1 0 0 1-1.5.1l-5-5a1 1 0 1 1 1.4-1.4l4.1 4.1 7.4-11Z" fill-rule="nonzero"/></svg>',
'chevron-down': '<svg width="10" height="10"><path d="M8.7 2.2c.3-.3.8-.3 1 0 .4.4.4.9 0 1.2L5.7 7.8c-.3.3-.9.3-1.2 0L.2 3.4a.8.8 0 0 1 0-1.2c.3-.3.8-.3 1.1 0L5 6l3.7-3.8Z" fill-rule="nonzero"/></svg>',
'chevron-left': '<svg width="10" height="10"><path d="M7.8 1.3 4 5l3.8 3.7c.3.3.3.8 0 1-.4.4-.9.4-1.2 0L2.2 5.7a.8.8 0 0 1 0-1.2L6.6.2C7 0 7.4 0 7.8.2c.3.3.3.8 0 1.1Z" fill-rule="nonzero"/></svg>',
'chevron-right': '<svg width="10" height="10"><path d="M2.2 1.3a.8.8 0 0 1 0-1c.4-.4.9-.4 1.2 0l4.4 4.1c.3.4.3.9 0 1.2L3.4 9.8c-.3.3-.8.3-1.2 0a.8.8 0 0 1 0-1.1L6 5 2.2 1.3Z" fill-rule="nonzero"/></svg>',
'chevron-up': '<svg width="10" height="10"><path d="M8.7 7.8 5 4 1.3 7.8c-.3.3-.8.3-1 0a.8.8 0 0 1 0-1.2l4.1-4.4c.3-.3.9-.3 1.2 0l4.2 4.4c.3.3.3.9 0 1.2-.3.3-.8.3-1.1 0Z" fill-rule="nonzero"/></svg>',
'close-filled': '<svg width="24" height="24"><path fill-rule="evenodd" d="M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20Zm4.7 5.3a1 1 0 0 0-1.3 0L12 10.6 8.6 7.3a1 1 0 0 0-1.3 1.3l3.3 3.4-3.3 3.4a1 1 0 0 0 1.3 1.3l3.4-3.3 3.4 3.3a1 1 0 0 0 1.3-1.3L13.4 12l3.3-3.4a1 1 0 0 0 0-1.3Z"/></svg>',
'close': '<svg width="24" height="24"><path d="M17.3 8.2 13.4 12l3.9 3.8a1 1 0 0 1-1.5 1.5L12 13.4l-3.8 3.9a1 1 0 0 1-1.5-1.5l3.9-3.8-3.9-3.8a1 1 0 0 1 1.5-1.5l3.8 3.9 3.8-3.9a1 1 0 0 1 1.5 1.5Z" fill-rule="evenodd"/></svg>',
'code-sample': '<svg width="24" height="26"><path d="M7.1 11a2.8 2.8 0 0 1-.8 2 2.8 2.8 0 0 1 .8 2v1.7c0 .3.1.6.4.8.2.3.5.4.8.4.3 0 .4.2.4.4v.8c0 .2-.1.4-.4.4-.7 0-1.4-.3-2-.8-.5-.6-.8-1.3-.8-2V15c0-.3-.1-.6-.4-.8-.2-.3-.5-.4-.8-.4a.4.4 0 0 1-.4-.4v-.8c0-.2.2-.4.4-.4.3 0 .6-.1.8-.4.3-.2.4-.5.4-.8V9.3c0-.7.3-1.4.8-2 .6-.5 1.3-.8 2-.8.3 0 .4.2.4.4v.8c0 .2-.1.4-.4.4-.3 0-.6.1-.8.4-.3.2-.4.5-.4.8V11Zm9.8 0V9.3c0-.3-.1-.6-.4-.8-.2-.3-.5-.4-.8-.4a.4.4 0 0 1-.4-.4V7c0-.2.1-.4.4-.4.7 0 1.4.3 2 .8.5.6.8 1.3.8 2V11c0 .3.1.6.4.8.2.3.5.4.8.4.2 0 .4.2.4.4v.8c0 .2-.2.4-.4.4-.3 0-.6.1-.8.4-.3.2-.4.5-.4.8v1.7c0 .7-.3 1.4-.8 2-.6.5-1.3.8-2 .8a.4.4 0 0 1-.4-.4v-.8c0-.2.1-.4.4-.4.3 0 .6-.1.8-.4.3-.2.4-.5.4-.8V15a2.8 2.8 0 0 1 .8-2 2.8 2.8 0 0 1-.8-2Zm-3.3-.4c0 .4-.1.8-.5 1.1-.3.3-.7.5-1.1.5-.4 0-.8-.2-1.1-.5-.4-.3-.5-.7-.5-1.1 0-.5.1-.9.5-1.2.3-.3.7-.4 1.1-.4.4 0 .8.1 1.1.4.4.3.5.7.5 1.2ZM12 13c.4 0 .8.1 1.1.5.4.3.5.7.5 1.1 0 1-.1 1.6-.5 2a3 3 0 0 1-1.1 1c-.4.3-.8.4-1.1.4a.5.5 0 0 1-.5-.5V17a3 3 0 0 0 1-.2l.6-.6c-.6 0-1-.2-1.3-.5-.2-.3-.3-.7-.3-1 0-.5.1-1 .5-1.2.3-.4.7-.5 1.1-.5Z" fill-rule="evenodd"/></svg>',
'collapse': '<svg width="24" height="24"><path d="M4 3a1 1 0 0 1 1 1v16a1 1 0 0 1-2 0V4a1 1 0 0 1 1-1Zm11 4.6a1 1 0 0 1 1.4 0L21 12l-4.6 4.6a1 1 0 0 1-1.4-1.4l2.2-2.2H10a1 1 0 1 1 0-2h7.2L15 9a1 1 0 0 1 0-1.4Z"/></svg>',
'color-levels': '<svg width="24" height="24"><path d="M17.5 11.4A9 9 0 0 1 18 14c0 .5 0 1-.2 1.4 0 .4-.3.9-.5 1.3a6.2 6.2 0 0 1-3.7 3 5.7 5.7 0 0 1-3.2 0A5.9 5.9 0 0 1 7.6 18a6.2 6.2 0 0 1-1.4-2.6 6.7 6.7 0 0 1 0-2.8c0-.4.1-.9.3-1.3a13.6 13.6 0 0 1 2.3-4A20 20 0 0 1 12 4a26.4 26.4 0 0 1 3.2 3.4 18.2 18.2 0 0 1 2.3 4Zm-2 4.5c.4-.7.5-1.4.5-2a7.3 7.3 0 0 0-1-3.2c.2.6.2 1.2.2 1.9a4.5 4.5 0 0 1-1.3 3 5.3 5.3 0 0 1-2.3 1.5 4.9 4.9 0 0 1-2 .1 4.3 4.3 0 0 0 2.4.8 4 4 0 0 0 2-.6 4 4 0 0 0 1.5-1.5Z" fill-rule="evenodd"/></svg>',
'color-picker': '<svg width="24" height="24"><path d="M12 3a9 9 0 0 0 0 18 1.5 1.5 0 0 0 1.1-2.5c-.2-.3-.4-.6-.4-1 0-.8.7-1.5 1.5-1.5H16a5 5 0 0 0 5-5c0-4.4-4-8-9-8Zm-5.5 9a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3Zm3-4a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3Zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3Zm3 4a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3Z" fill-rule="nonzero"/></svg>',
'color-swatch-remove-color': '<svg width="24" height="24" class="color-swatch-remove-color"><path stroke="#000" stroke-width="2" d="M21 3 3 21" fill-rule="evenodd"/></svg>',
'color-swatch': '<svg width="24" height="24"><rect x="3" y="3" width="18" height="18" rx="1" fill-rule="evenodd"/></svg>',
'comment-add': '<svg width="24" height="24"><g fill-rule="nonzero"><path d="m9 19 3-2h7c.6 0 1-.4 1-1V6c0-.6-.4-1-1-1H5a1 1 0 0 0-1 1v10c0 .6.4 1 1 1h4v2Zm-2 4v-4H5a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3h-6.4L7 23Z"/><path d="M13 10h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0v-2H9a1 1 0 0 1 0-2h2V8a1 1 0 0 1 2 0v2Z"/></g></svg>',
'comment': '<svg width="24" height="24"><path fill-rule="nonzero" d="m9 19 3-2h7c.6 0 1-.4 1-1V6c0-.6-.4-1-1-1H5a1 1 0 0 0-1 1v10c0 .6.4 1 1 1h4v2Zm-2 4v-4H5a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3h-6.4L7 23Z"/></svg>',
'continue-writing': '<svg height="24" width="24"><path d="M19.6 14.1a1.5 1.5 0 0 1 1 .5l1 .9.3.5.1.5-.1.6a1.3 1.3 0 0 1-.3.5L16 23H13v-3l5.5-5.5.5-.3h.6zm-5.1 6.5v1h1l4-4-1-1z" fill-rule="evenodd"/><path d="M10 12.9H2v-2h8zm4-4H2V7h12zm0-4H2V3h12zm4.2-1.7c0-.3.5-.3.7 0a4.4 4.4 0 0 0 2.6 2.6.4.4 0 0 1 0 .7l-.2.1a4.4 4.4 0 0 0-2.4 2.5.4.4 0 0 1-.7 0 4.4 4.4 0 0 0-2.5-2.5h-.2a.4.4 0 0 1 0-.8 4.4 4.4 0 0 0 2.6-2.6z"/></svg>',
'contrast': '<svg width="24" height="24"><path fill-rule="evenodd" d="M12 4a7.8 7.8 0 0 1 5.7 2.3A8 8 0 0 1 19.4 9c.4 1 .6 2 .6 3.1s-.2 2.1-.6 3.1a8 8 0 0 1-1.7 2.6 8 8 0 0 1-2.6 1.7 7.8 7.8 0 0 1-3.1.6 7.8 7.8 0 0 1-3.1-.6 8 8 0 0 1-2.6-1.7A8 8 0 0 1 4.6 15 7.8 7.8 0 0 1 4 12c0-1.1.2-2.1.6-3.1a8 8 0 0 1 1.7-2.6A8 8 0 0 1 9 4.6 7.8 7.8 0 0 1 12 4Zm-6 8a6 6 0 0 0 1.8 4.2 6 6 0 0 0 1.9 1.3 5.8 5.8 0 0 0 2.3.5V6c-.8 0-1.6.2-2.3.5a6 6 0 0 0-2 1.3 6 6 0 0 0-1.2 1.9A5.8 5.8 0 0 0 6 12Z"/></svg>',
'copy': '<svg width="24" height="24"><path d="M16 3H6a2 2 0 0 0-2 2v11h2V5h10V3Zm1 4a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-7a2 2 0 0 1-2-2V9c0-1.2.9-2 2-2h7Zm0 12V9h-7v10h7Z" fill-rule="nonzero"/></svg>',
'crop': '<svg width="24" height="24"><path d="M17 8v7h2c.6 0 1 .4 1 1s-.4 1-1 1h-2v2c0 .6-.4 1-1 1a1 1 0 0 1-1-1v-2H7V9H5a1 1 0 1 1 0-2h2V5c0-.6.4-1 1-1s1 .4 1 1v2h7l3-3 1 1-3 3ZM9 9v5l5-5H9Zm1 6h5v-5l-5 5Z" fill-rule="evenodd"/></svg>',
'cut-column': '<svg width="24" height="24"><path fill-rule="evenodd" d="M7.2 4.5c.9 0 1.6.4 2.2 1A3.7 3.7 0 0 1 10.5 8v.5l1 1 4-4 1-.5a3.3 3.3 0 0 1 2 0c.4 0 .7.3 1 .5L17 8h4v13h-6V10l-1.5 1.5.5.5v4l-2.5-2.5-1 1v.5c0 .4 0 .8-.3 1.2-.2.5-.4.9-.8 1.2-.6.7-1.3 1-2.2 1-.8.2-1.5 0-2-.6l-.5-.8-.2-1c0-.4 0-.8.3-1.2A3.9 3.9 0 0 1 7 12.7c.5-.2 1-.3 1.5-.2l1-1-1-1c-.5 0-1 0-1.5-.2-.5-.1-1-.4-1.4-.9-.4-.3-.6-.7-.8-1.2L4.5 7c0-.4 0-.7.2-1 0-.3.3-.6.5-.8.5-.5 1.2-.8 2-.7Zm12.3 5h-3v10h3v-10ZM8 13.8h-.3l-.4.2a2.8 2.8 0 0 0-.7.4v.1a2.8 2.8 0 0 0-.6.8l-.1.4v.7l.2.5.5.2h.7a2.6 2.6 0 0 0 .8-.3 2.4 2.4 0 0 0 .7-.7 2.5 2.5 0 0 0 .3-.8 1.5 1.5 0 0 0 0-.8 1 1 0 0 0-.2-.4 1 1 0 0 0-.5-.2H8Zm3.5-3.7c-.4 0-.7.1-1 .4-.3.3-.4.6-.4 1s.1.7.4 1c.3.3.6.4 1 .4s.7-.1 1-.4c.3-.3.4-.6.4-1s-.1-.7-.4-1c-.3-.3-.6-.4-1-.4ZM7 5.8h-.4a1 1 0 0 0-.5.3 1 1 0 0 0-.2.5v.7a2.5 2.5 0 0 0 .3.8l.2.3h.1l.4.4.4.2.4.1h.7L9 9l.2-.4a1.6 1.6 0 0 0 0-.8 2.6 2.6 0 0 0-.3-.8A2.5 2.5 0 0 0 7.7 6l-.4-.1H7Z"/></svg>',
'cut-row': '<svg width="24" height="24"><path fill-rule="evenodd" d="M22 3v5H9l3 3 2-2h4l-4 4 1 1h.5c.4 0 .8 0 1.2.3.5.2.9.4 1.2.8.7.6 1 1.3 1 2.2.2.8 0 1.5-.6 2l-.8.5-1 .2c-.4 0-.8 0-1.2-.3a3.9 3.9 0 0 1-2.1-2.2c-.2-.5-.3-1-.2-1.5l-1-1-1 1c0 .5 0 1-.2 1.5-.1.5-.4 1-.9 1.4-.3.4-.7.6-1.2.8l-1.2.3c-.4 0-.7 0-1-.2-.3 0-.6-.3-.8-.5-.5-.5-.8-1.2-.7-2 0-.9.4-1.6 1-2.2A3.7 3.7 0 0 1 8.6 14H9l1-1-4-4-.5-1a3.3 3.3 0 0 1 0-2c0-.4.3-.7.5-1l2 2V3h14ZM8.5 15.3h-.3a2.6 2.6 0 0 0-.8.4 2.5 2.5 0 0 0-.9 1.1l-.1.4v.7l.2.5.5.2h.7a2.5 2.5 0 0 0 .8-.3L9 18V18l.4-.4.2-.4.1-.4v-.7a1 1 0 0 0-.2-.5 1 1 0 0 0-.4-.2h-.5Zm7 0H15a1 1 0 0 0-.4.3 1 1 0 0 0-.2.5 1.5 1.5 0 0 0 0 .7v.4a2.8 2.8 0 0 0 .5.7h.1a2.8 2.8 0 0 0 .8.6l.4.1h.7l.5-.2.2-.5v-.7a2.6 2.6 0 0 0-.3-.8 2.4 2.4 0 0 0-.7-.7 2.5 2.5 0 0 0-.8-.3h-.3ZM12 11.6c-.4 0-.7.1-1 .4-.3.3-.4.6-.4 1s.1.7.4 1c.3.3.6.4 1 .4s.7-.1 1-.4c.3-.3.4-.6.4-1s-.1-.7-.4-1c-.3-.3-.6-.4-1-.4Zm8.5-7.1h-11v2h11v-2Z"/></svg>',
'cut': '<svg width="24" height="24"><path d="M18 15c.6.7 1 1.4 1 2.3 0 .8-.2 1.5-.7 2l-.8.5-1 .2c-.4 0-.8 0-1.2-.3a3.9 3.9 0 0 1-2.1-2.2c-.2-.5-.3-1-.2-1.5l-1-1-1 1c0 .5 0 1-.2 1.5-.1.5-.4 1-.9 1.4-.3.4-.7.6-1.2.8l-1.2.3c-.4 0-.7 0-1-.2-.3 0-.6-.3-.8-.5-.5-.5-.8-1.2-.7-2 0-.9.4-1.6 1-2.2A3.7 3.7 0 0 1 8.6 14H9l1-1-4-4-.5-1a3.3 3.3 0 0 1 0-2c0-.4.3-.7.5-1l6 6 6-6 .5 1a3.3 3.3 0 0 1 0 2c0 .4-.3.7-.5 1l-4 4 1 1h.5c.4 0 .8 0 1.2.3.5.2.9.4 1.2.8Zm-8.5 2.2.1-.4v-.7a1 1 0 0 0-.2-.5 1 1 0 0 0-.4-.2 1.6 1.6 0 0 0-.8 0 2.6 2.6 0 0 0-.8.3 2.5 2.5 0 0 0-.9 1.1l-.1.4v.7l.2.5.5.2h.7a2.5 2.5 0 0 0 .8-.3 2.8 2.8 0 0 0 1-1Zm2.5-2.8c.4 0 .7-.1 1-.4.3-.3.4-.6.4-1s-.1-.7-.4-1c-.3-.3-.6-.4-1-.4s-.7.1-1 .4c-.3.3-.4.6-.4 1s.1.7.4 1c.3.3.6.4 1 .4Zm5.4 4 .2-.5v-.7a2.6 2.6 0 0 0-.3-.8 2.4 2.4 0 0 0-.7-.7 2.5 2.5 0 0 0-.8-.3 1.5 1.5 0 0 0-.8 0 1 1 0 0 0-.4.2 1 1 0 0 0-.2.5 1.5 1.5 0 0 0 0 .7v.4l.3.4.3.4a2.8 2.8 0 0 0 .8.5l.4.1h.7l.5-.2Z" fill-rule="evenodd"/></svg>',
'document-gear-properties': '<svg height="24" width="24"><path d="M17.3 14.5a7.1 7.1 0 0 1 1.2.6l1.5-.5 1 1.8-1.2 1V18.7l1.2 1-1 1.6-1.5-.4a4.5 4.5 0 0 1-.6.3 7.1 7.1 0 0 1-.6.3L17 23h-2l-.3-1.5a7.4 7.4 0 0 1-.6-.3 4.5 4.5 0 0 1-.6-.3l-1.5.5-1-1.8 1.2-1a3.9 3.9 0 0 1 0-.6v-.7l-1.2-1 1-1.6 1.5.4a4.5 4.5 0 0 1 1.2-.6L15 13h2zM16 16a2 2 0 0 0-1.5.6A1.9 1.9 0 0 0 14 18c0 .6.2 1 .6 1.4s1 .6 1.5.6 1-.2 1.5-.6.6-.8.6-1.4-.2-1-.6-1.4A2 2 0 0 0 16 16z" fill-rule="evenodd"/><path d="M19 7.6V12h-2V9h-4V5H7v14h3v2H7a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2h7.4z"/></svg>',
'document-properties': '<svg width="24" height="24"><path d="M14.4 3H7a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h10a2 2 0 0 0 2-2V7.6L14.4 3ZM17 19H7V5h6v4h4v10Z" fill-rule="nonzero"/></svg>',
'drag': '<svg width="24" height="24"><path d="M13 5h2v2h-2V5Zm0 4h2v2h-2V9ZM9 9h2v2H9V9Zm4 4h2v2h-2v-2Zm-4 0h2v2H9v-2Zm0 4h2v2H9v-2Zm4 0h2v2h-2v-2ZM9 5h2v2H9V5Z" fill-rule="evenodd"/></svg>',
'dropbox': '<svg height="24" width="24"><path clip-rule="evenodd" d="m6.4 9.3-3-2a1 1 0 0 1 0-1.6l4-2.5a1 1 0 0 1 1 0L12 5.5l3.6-2.3a1 1 0 0 1 1 0l4 2.5a1 1 0 0 1 0 1.7l-3 1.9 3 1.9a1 1 0 0 1 0 1.6l-2.9 1.9V16c0 .3-.1.6-.4.8l-4.5 3a1 1 0 0 1-1.2 0l-4.4-3a1 1 0 0 1-.5-.9v-1l-3.2-2.2a1 1 0 0 1 0-1.6zm3 0L12 7.5l2.6 1.8L12 11zM8.9 15v.4l3.3 2.3 3.4-2.3v-.2L12 13l-3.1 2zm-1-10-2 1.4 2 1.3 2-1.3zm8.2 0-2 1.4 2 1.3 2-1.3zm-2 6.9 2-1.3 2 1.3-2 1.3-2-1.3zm-8.3 0 2-1.3L10 12l-2 1.3-2-1.3z" fill-rule="evenodd"/></svg>',
'duplicate-column': '<svg width="24" height="24"><path d="M17 6v16h-7V6h7Zm-2 2h-3v12h3V8Zm-2-6v2H8v15H6V2h7Z"/></svg>',
'duplicate-row': '<svg width="24" height="24"><path d="M22 11v7H6v-7h16Zm-2 2H8v3h12v-3Zm-1-6v2H4v5H2V7h17Z"/></svg>',
'duplicate': '<svg width="24" height="24"><g fill-rule="nonzero"><path d="M16 3v2H6v11H4V5c0-1.1.9-2 2-2h10Zm3 8h-2V9h-7v10h9a2 2 0 0 1-2 2h-7a2 2 0 0 1-2-2V9c0-1.2.9-2 2-2h7a2 2 0 0 1 2 2v2Z"/><path d="M17 14h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0v-1h-1a1 1 0 0 1 0-2h1v-1a1 1 0 0 1 2 0v1Z"/></g></svg>',
'edit-block': '<svg height="24" width="24"><path d="M14.7 3.2a2 2 0 0 1 2.9 0L20.4 6c.8.8.8 2.1 0 2.9l-10 10a2 2 0 0 1-1.1.6L3 20.6l1-6.2c.2-.5.3-.8.6-1.1zM6.1 14.7 5.5 18l3.4-.6-2.8-2.9zm1.4-1.5 2.9 2.9 6.2-6.3L13.8 7zm7.7-7.6L18 8.4l1-1L16 4.7z" fill-rule="evenodd"/></svg>',
'edit-image': '<svg width="24" height="24"><path d="M18 16h2V7a2 2 0 0 0-2-2H7v2h11v9ZM6 17h15a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0v-1H6a2 2 0 0 1-2-2V7H3a1 1 0 1 1 0-2h1V4a1 1 0 1 1 2 0v13Zm3-5.3 1.3 2 3-4.7 3.7 6H7l2-3.3Z" fill-rule="nonzero"/></svg>',
'embed-page': '<svg width="24" height="24"><path d="M19 6V5H5v14h2A13 13 0 0 1 19 6Zm0 1.4c-.8.8-1.6 2.4-2.2 4.6H19V7.4Zm0 5.6h-2.4c-.4 1.8-.6 3.8-.6 6h3v-6Zm-4 6c0-2.2.2-4.2.6-6H13c-.7 1.8-1.1 3.8-1.1 6h3Zm-4 0c0-2.2.4-4.2 1-6H9.6A12 12 0 0 0 8 19h3ZM4 3h16c.6 0 1 .4 1 1v16c0 .6-.4 1-1 1H4a1 1 0 0 1-1-1V4c0-.6.4-1 1-1Zm11.8 9c.4-1.9 1-3.4 1.8-4.5a9.2 9.2 0 0 0-4 4.5h2.2Zm-3.4 0a12 12 0 0 1 2.8-4 12 12 0 0 0-5 4h2.2Z" fill-rule="nonzero"/></svg>',
'embed': '<svg width="24" height="24"><path d="M4 3h16c.6 0 1 .4 1 1v16c0 .6-.4 1-1 1H4a1 1 0 0 1-1-1V4c0-.6.4-1 1-1Zm1 2v14h14V5H5Zm4.8 2.6 5.6 4a.5.5 0 0 1 0 .8l-5.6 4A.5.5 0 0 1 9 16V8a.5.5 0 0 1 .8-.4Z" fill-rule="nonzero"/></svg>',
'emoji': '<svg width="24" height="24"><path d="M9 11c.6 0 1-.4 1-1s-.4-1-1-1a1 1 0 0 0-1 1c0 .6.4 1 1 1Zm6 0c.6 0 1-.4 1-1s-.4-1-1-1a1 1 0 0 0-1 1c0 .6.4 1 1 1Zm-3 5.5c2.1 0 4-1.5 4.4-3.5H7.6c.5 2 2.3 3.5 4.4 3.5ZM12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm0 14.5a6.5 6.5 0 1 1 0-13 6.5 6.5 0 0 1 0 13Z" fill-rule="nonzero"/></svg>',
'evernote': '<svg width="24" height="24"><path fill-rule="evenodd" d="m9.5 3-.3.1-2.6 2.6C3.8 8.5 4 8.2 4 8.7c0 1.3.3 3 .6 4.3.5 2 1.1 3.2 2 3.7l1.2.3c1 .2 1.5.2 2.2.2 1.3 0 1.7 0 2.2-.4.3-.2.3-.4.4-1a38.4 38.4 0 0 1 .3-.4l.3.1 1.8.1h1.6v-1.8H14l-.1-.2a6.8 6.8 0 0 1-.5-1c-.2-.4-.3-.6-.5-.7a.8.8 0 0 0-.5-.1.9.9 0 0 0-.8.5l-.4 1.6-.3 1.5H9.4l-2-.3a1 1 0 0 1-.1-.2A10.1 10.1 0 0 1 6 9.7v-.2h4l.5-.5V5H12c.4.2.7.7.8 1.1a1 1 0 0 0 .2.6c.2.2.3.2 1 .3 1.2 0 2 .2 2.5.6.4.2.8.7 1 1.2.3 1.2.5 4 .3 6.4 0 1.8-.4 3.4-.8 4h-.7a8 8 0 0 1-.8 0c-.2 0-.3-.3-.2-.5a.5.5 0 0 1 .4-.3h.9v-1.8H16c-.7 0-.9 0-1.2.2-.9.4-1.4 1.2-1.4 2.1l.1.4.1.2a1 1 0 0 1 .1.2c.3.7.8 1.2 1.4 1.3l1.2.1h1.2a2 2 0 0 0 1-.8 9 9 0 0 0 1.1-4.3c.2-3 0-6.3-.4-7.7A4.3 4.3 0 0 0 16 5.3a7 7 0 0 0-1.3-.2 7.2 7.2 0 0 1-.4 0 5.5 5.5 0 0 1 0-.3c-.2-.5-.6-1-1.2-1.4a3 3 0 0 0-.5-.2c-.4-.2-.5-.2-1.8-.2a26.5 26.5 0 0 0-1.3 0Zm-.8 4v.7H7.2l.7-.8.8-.7v.7Zm7 3.5c-.4 0-.7.5-.9 1v.7h.3c.7 0 1.1.2 1.5.6l.3.2.3-.6V11a1 1 0 0 0-.6-.4 2 2 0 0 0-.8 0Z" clip-rule="evenodd"/></svg>',
'explain': '<svg height="24" width="24"><path d="M11.6 3.1a4 4 0 0 1 3 6.7 6.8 6.8 0 0 1 1.7 1.2c1 0 2.2 0 3.5.3a1 1 0 0 1 .8.9v8a1 1 0 0 1-1.1 1c-1.5-.3-2.6-.3-3.7-.1-1 .2-2.2.6-3.7 1.4a1 1 0 0 1-.3.1 1 1 0 0 1-.6 0c-1.6-.9-2.7-1.3-3.8-1.5-1-.2-2.2-.2-3.7 0a1 1 0 0 1-1.1-1v-7.9a1 1 0 0 1 .8-1C4.7 11 5.8 11 7 11a6.8 6.8 0 0 1 1.3-1l.4-.2a4 4 0 0 1 3-6.7zm-4 10h-3V19a11.3 11.3 0 0 1 6 1v-5.7c-1.2-.6-2.2-1-3-1.2zm11 0a8.6 8.6 0 0 0-6 1.2V20l2.8-.9c1-.2 2-.2 3.2-.1zm-7-2a4.6 4.6 0 0 0-2.1.5l2.1 1 2.1-1a4.6 4.6 0 0 0-2.1-.5zm0-6a2 2 0 1 0 0 4 2 2 0 0 0 0-4z" fill-rule="evenodd"/><path d="M18.7 2.2a.3.3 0 0 1 .6 0 4.1 4.1 0 0 0 2.5 2.5.3.3 0 0 1 0 .6l-.2.1a4.1 4.1 0 0 0-2.3 2.4c-.1.3-.5.3-.6 0a4.1 4.1 0 0 0-2.3-2.4h-.2a.3.3 0 0 1 0-.7 4.1 4.1 0 0 0 2.5-2.5z"/></svg>',
'export-pdf': '<svg width="24" height="24"><path fill-rule="evenodd" clip-rule="evenodd" d="M7 3h7.4L19 7.6V17h-2V9h-4V5H7v3H5V5c0-1.1.9-2 2-2Z"/><path d="M2.6 15.2v-1.9h1c.6 0 1-.2 1.4-.5.3-.3.5-.7.5-1.2s-.2-.9-.5-1.2a2 2 0 0 0-1.3-.4H1v5.2h1.6Zm.4-3h-.4v-1.1h.5l.6.1.2.5c0 .1 0 .3-.2.4l-.7.1Zm5.7 3 1-.1c.3 0 .5-.2.7-.4l.5-.8c.2-.3.2-.7.2-1.3v-1l-.5-.8c-.2-.3-.4-.5-.7-.6L8.7 10H6.3v5.2h2.4Zm-.4-1.1H8v-3h.4c.5 0 .8.2 1 .4l.2 1.1-.1 1-.3.3-.8.2Zm5.3 1.2V13h2v-1h-2v-1H16V10h-4v5.2h1.6Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M15 17a1 1 0 1 0-2 0v3.1l-1.4-1a1 1 0 1 0-1.2 1.7l3.6 2.4 3.6-2.4a1 1 0 0 0-1.2-1.6l-1.4 1V17Z"/></svg>',
'export-word': '<svg width="24" height="24"><path d="M9.5 7A1.5 1.5 0 0 1 11 8.4v7.1A1.5 1.5 0 0 1 9.6 17H2.5A1.5 1.5 0 0 1 1 15.6V8.5A1.5 1.5 0 0 1 2.4 7h7.1Zm-1 2.8-1 2.6-1-2.5v-.1a.6.6 0 0 0-1 0l-.1.1-.9 2.5-1-2.5v-.1a.6.6 0 0 0-1 .4v.1l1.5 4v.1a.6.6 0 0 0 1 0v-.1l1-2.5.9 2.5v.1a.6.6 0 0 0 1 0H8l1.6-4v-.2a.6.6 0 0 0-1.1-.4Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M7 3h7.4L19 7.6V17h-2V9h-4V5H5c0-1.1.9-2 2-2ZM15 17a1 1 0 1 0-2 0v3.1l-1.4-1a1 1 0 1 0-1.2 1.7l3.6 2.4 3.6-2.4a1 1 0 0 0-1.2-1.6l-1.4 1V17Z"/></svg>',
'export': '<svg width="24" height="24"><g fill-rule="nonzero"><path d="M14.4 3 18 7v1h-5V5H7v14h9a1 1 0 0 1 2 0c0 1-.8 2-1.9 2H7c-1 0-2-.8-2-1.9V5c0-1 .8-2 1.9-2h7.5Z"/><path d="M18.1 12c.5 0 .9.4.9 1 0 .5-.3 1-.8 1h-7.3c-.5 0-.9-.4-.9-1 0-.5.3-1 .8-1h7.3Z"/><path d="M16.4 9.2a1 1 0 0 1 1.4.2l2.4 3.6-2.4 3.6a1 1 0 0 1-1.7-1v-.2l1.7-2.4-1.6-2.4a1 1 0 0 1 .2-1.4Z"/></g></svg>',
'exposure': '<svg width="24" height="24"><path d="M5 21a2 2 0 0 1-1.4-.6A2 2 0 0 1 3 19V5c0-.6.2-1 .6-1.4A2 2 0 0 1 5 3h14c.6 0 1 .2 1.4.6.4.4.6.8.6 1.4v14c0 .6-.2 1-.6 1.4a2 2 0 0 1-1.4.6H5Zm0-2h14V5L5 19Zm9.5-1v-2h-2v-1.5h2v-2H16v2h2V16h-2v2h-1.5ZM6 8.5h5V7H6v1.5Z"/></svg>',
'fb': '<svg width="24" height="24"><path fill-rule="evenodd" d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-11 6.7V14H8.1v-2.5h2v-1C10 8.2 11.5 7 12.8 7H16v2.5h-2.4c-.8 0-1 .7-1 1.3v.7h3.2l-1 2.5h-2v5a7 7 0 1 0-2.8-.2Z" clip-rule="evenodd"/></svg>',
'feedback': '<svg width="24" height="24"><path fill-rule="evenodd" d="M16 6c1 0 2 1 2 2v5c0 1-1 2-2 2h-3l-4 3v-3H8c-1 0-2-1-2-2V8c0-1 1-2 2-2h8Zm-7 5v1h4v-1H9Zm0-2v1h6V9H9Z"/></svg>',
'fill': '<svg width="24" height="26"><path d="m16.6 12-9-9-1.4 1.4 2.4 2.4-5.2 5.1c-.5.6-.5 1.6 0 2.2L9 19.6a1.5 1.5 0 0 0 2.2 0l5.5-5.5c.5-.6.5-1.6 0-2.2ZM5.2 13 10 8.2l4.8 4.8H5.2ZM19 14.5s-2 2.2-2 3.5c0 1.1.9 2 2 2a2 2 0 0 0 2-2c0-1.3-2-3.5-2-3.5Z" fill-rule="nonzero"/></svg>',
'fix-grammar': '<svg height="24" width="24"><path d="M22 14.5 14.4 22l-4.6-4.5 1.5-1.5 3 3 6.2-6z"/><path d="M15 15.8h-2.5l-1.2-3.5H5.6l-1.2 3.5H2L7.2 2h2.6zm-8.6-5.5h4.2l-2-5.7h-.2z" fill-rule="evenodd"/><path d="M18.1 2.2c.2-.3.6-.3.8 0a4.4 4.4 0 0 0 2.6 2.6.4.4 0 0 1 0 .7l-.2.1a4.4 4.4 0 0 0-2.4 2.5.4.4 0 0 1-.7 0 4.4 4.4 0 0 0-2.5-2.5h-.2a.4.4 0 0 1 0-.8 4.4 4.4 0 0 0 2.6-2.6z"/></svg>',
'flickr': '<svg width="24" height="24"><path d="M8.2 15.3c1.7 0 3.1-1.4 3.1-3.1A3.2 3.2 0 0 0 8.1 9 3.2 3.2 0 0 0 5 12.2c0 1.7 1.4 3 3.2 3Zm7.6 0c1.7 0 3.2-1.4 3.2-3.1a3.2 3.2 0 0 0-6.3 0c0 1.7 1.4 3 3.1 3Z"/></svg>',
'flip-horizontally': '<svg width="24" height="24"><path d="M14 19h2v-2h-2v2Zm4-8h2V9h-2v2ZM4 7v10c0 1.1.9 2 2 2h3v-2H6V7h3V5H6a2 2 0 0 0-2 2Zm14-2v2h2a2 2 0 0 0-2-2Zm-7 16h2V3h-2v18Zm7-6h2v-2h-2v2Zm-4-8h2V5h-2v2Zm4 12a2 2 0 0 0 2-2h-2v2Z" fill-rule="nonzero"/></svg>',
'flip-vertically': '<svg width="24" height="24"><path d="M5 14v2h2v-2H5Zm8 4v2h2v-2h-2Zm4-14H7a2 2 0 0 0-2 2v3h2V6h10v3h2V6a2 2 0 0 0-2-2Zm2 14h-2v2a2 2 0 0 0 2-2ZM3 11v2h18v-2H3Zm6 7v2h2v-2H9Zm8-4v2h2v-2h-2ZM5 18c0 1.1.9 2 2 2v-2H5Z" fill-rule="nonzero"/></svg>',
'folder': '<svg width="24" height="24"><path fill-rule="evenodd" d="M5.6 6a.6.6 0 0 0-.6.6v11.2a.6.6 0 0 0 .6.6h12.8a.6.6 0 0 0 .6-.6V9a.6.6 0 0 0-.6-.6h-7.2a1 1 0 0 1-.8-.4L9 6H5.6ZM3.8 4.8A2.6 2.6 0 0 1 5.6 4h4a1 1 0 0 1 .8.4l1.3 2h6.7A2.6 2.6 0 0 1 21 9v8.8a2.6 2.6 0 0 1-2.6 2.6H5.6A2.6 2.6 0 0 1 3 17.8V6.6c0-.7.3-1.4.8-1.8Z" clip-rule="evenodd"/></svg>',
'footnote': '<svg width="24" height="24"><path d="M19 13c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2h14Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M19 4v6h-1V5h-1.5V4h2.6Z"/><path d="M12 18c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2h7ZM14 8c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2h9Z"/></svg>',
'format-code': '<svg width="24" height="24"><path d="m10 22 2-7H6l9-13h2l-2 8h7L12 22h-2ZM6 2h7l-1.4 2H6V2Zm4.2 4H4v2h4.8l1.4-2Zm-2.7 4H2v2h4l1.5-2Z"/></svg>',
'format-painter': '<svg width="24" height="24"><path d="M18 5V4c0-.5-.4-1-1-1H5a1 1 0 0 0-1 1v4c0 .6.5 1 1 1h12c.6 0 1-.4 1-1V7h1v4H9v9c0 .6.4 1 1 1h2c.6 0 1-.4 1-1v-7h8V5h-3Z" fill-rule="nonzero"/></svg>',
'format': '<svg width="24" height="24"><path fill-rule="evenodd" d="M17 5a1 1 0 0 1 0 2h-4v11a1 1 0 0 1-2 0V7H7a1 1 0 1 1 0-2h10Z"/></svg>',
'fullscreen': '<svg width="24" height="24"><path d="m15.3 10-1.2-1.3 2.9-3h-2.3a.9.9 0 1 1 0-1.7H19c.5 0 .9.4.9.9v4.4a.9.9 0 1 1-1.8 0V7l-2.9 3Zm0 4 3 3v-2.3a.9.9 0 1 1 1.7 0V19c0 .5-.4.9-.9.9h-4.4a.9.9 0 1 1 0-1.8H17l-3-2.9 1.3-1.2ZM10 15.4l-2.9 3h2.3a.9.9 0 1 1 0 1.7H5a.9.9 0 0 1-.9-.9v-4.4a.9.9 0 1 1 1.8 0V17l2.9-3 1.2 1.3ZM8.7 10 5.7 7v2.3a.9.9 0 0 1-1.7 0V5c0-.5.4-.9.9-.9h4.4a.9.9 0 0 1 0 1.8H7l3 2.9-1.3 1.2Z" fill-rule="nonzero"/></svg>',
'gallery': '<svg width="24" height="24"><path fill-rule="nonzero" d="m5 15.7 2.3-2.2c.3-.3.7-.3 1 0L11 16l5.1-5c.3-.4.8-.4 1 0l2 1.9V8H5v7.7ZM5 18V19h3l1.8-1.9-2-2L5 17.9Zm14-3-2.5-2.4-6.4 6.5H19v-4ZM4 6h16c.6 0 1 .4 1 1v13c0 .6-.4 1-1 1H4a1 1 0 0 1-1-1V7c0-.6.4-1 1-1Zm6 7a2 2 0 1 1 0-4 2 2 0 0 1 0 4ZM4.5 4h15a.5.5 0 1 1 0 1h-15a.5.5 0 0 1 0-1Zm2-2h11a.5.5 0 1 1 0 1h-11a.5.5 0 0 1 0-1Z"/></svg>',
'gamma': '<svg height="24" width="24"><path d="M11.5 16.3a.7.7 0 1 0 1.4 0V14l2.7-6a.7.7 0 1 0-1.2-.6l-2.2 5-2.3-4.9C9.6 7 8.7 7 8 7.2v1.1c.8-.2.9 0 1.2 1l2.3 4.7z"/><path d="M4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm1 2h14v14H5z" fill-rule="evenodd"/></svg>',
'google-drive': '<svg height="24" width="24"><path clip-rule="evenodd" d="M8.7 4.5a1 1 0 0 1 .9-.5h4.8a1 1 0 0 1 .9.5l5.6 9.7a1 1 0 0 1 0 1l-2.4 4.2a1 1 0 0 1-.9.5H6.4a1 1 0 0 1-.9-.5l-2.4-4.2a1 1 0 0 1 0-1zM10.2 6l-4.5 7.7h2.5L12.7 6h-2.5zm4.2 1 4.4 7.7-1.2 2.2-4.4-7.7zM12 11.2l-1.5 2.5h3zm2.6 4.5H5.7L7 17.9h8.9l-1.3-2.2z" fill-rule="evenodd"/></svg>',
'google-photos': '<svg width="24" height="24"><path fill-rule="evenodd" d="M12.4 3c-.8 0-1.4.7-1.4 1.4v3.2a5 5 0 0 0-8 4c0 .8.6 1.4 1.4 1.4h3.2a5 5 0 0 0-1 3 5 5 0 0 0 5 5c.8 0 1.4-.6 1.4-1.4v-3.2a5 5 0 0 0 8-4c0-.8-.7-1.4-1.4-1.4h-3.2a5 5 0 0 0-4-8Zm.6 8V5a3 3 0 0 1 2.4 3 3 3 0 0 1-2.4 3Zm-2 0H5a3 3 0 0 1 3-2.4 3 3 0 0 1 3 2.4Zm2 2a3 3 0 0 0 3 2.4 3 3 0 0 0 3-2.4h-6Zm-4.4 3a3 3 0 0 1 2.4-3v6a3 3 0 0 1-2.4-3Z" clip-rule="evenodd"/></svg>',
'grayscale': '<svg height="24" width="24"><path d="M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2zm12.4 0H19v3.6zm-2.8 0h-2.2l8.6 8.6v-2.2zm-5 0H5l14 14v-2.6z" fill-rule="evenodd"/></svg>',
'help': '<svg width="24" height="24"><g fill-rule="evenodd"><path d="M12 5.5a6.5 6.5 0 0 0-6 9 6.3 6.3 0 0 0 1.4 2l1 1a6.3 6.3 0 0 0 3.6 1 6.5 6.5 0 0 0 6-9 6.3 6.3 0 0 0-1.4-2l-1-1a6.3 6.3 0 0 0-3.6-1ZM12 4a7.8 7.8 0 0 1 5.7 2.3A8 8 0 1 1 12 4Z"/><path d="M9.6 9.7a.7.7 0 0 1-.7-.8c0-1.1 1.5-1.8 3.2-1.8 1.8 0 3.2.8 3.2 2.4 0 1.4-.4 2.1-1.5 2.8-.2 0-.3.1-.3.2a2 2 0 0 0-.8.8.8.8 0 0 1-1.4-.6c.3-.7.8-1 1.3-1.5l.4-.2c.7-.4.8-.6.8-1.5 0-.5-.6-.9-1.7-.9-.5 0-1 .1-1.4.3-.2 0-.3.1-.3.2v-.2c0 .4-.4.8-.8.8Z" fill-rule="nonzero"/><circle cx="12" cy="16" r="1"/></g></svg>',
'highlight-bg-color': '<svg width="24" height="24"><g fill-rule="evenodd"><path class="tox-icon-highlight-bg-color__color" d="M3 18h18v3H3z"/><path fill-rule="nonzero" d="M7.7 16.7H3l3.3-3.3-.7-.8L10.2 8l4 4.1-4 4.2c-.2.2-.6.2-.8 0l-.6-.7-1.1 1.1zm5-7.5L11 7.4l3-2.9a2 2 0 0 1 2.6 0L18 6c.7.7.7 2 0 2.7l-2.9 2.9-1.8-1.8-.5-.6"/></g></svg>',
'highlight-key-points': '<svg height="24" width="24"><path d="M12 2c1.3 0 2.6.3 3.7.7l-.2.1a2.3 2.3 0 0 0-1.4 1.5 8 8 0 1 0 5.6 5.6 2.3 2.3 0 0 0 1.3-1v-.2h.1v-.2h.1v-.2A10 10 0 0 1 22 12 10 10 0 1 1 12 2z"/><path d="M12 11a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z"/><path d="M12 6.5a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11zm0 2a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7z" fill-rule="evenodd"/><path d="M18.7 2.2a.3.3 0 0 1 .6 0 4.1 4.1 0 0 0 2.5 2.5.3.3 0 0 1 0 .6l-.2.1a4.1 4.1 0 0 0-2.3 2.4c-.1.3-.5.3-.6 0a4.1 4.1 0 0 0-2.3-2.4h-.2A.3.3 0 0 1 16 5a.4.4 0 0 1 0-.2.3.3 0 0 1 .2 0v-.1h.1a5.2 5.2 0 0 0 .2-.2h.1a4.2 4.2 0 0 0 2-2.3z"/></svg>',
'home': '<svg width="24" height="24"><path fill-rule="nonzero" d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>',
'horizontal-rule': '<svg width="24" height="24"><path d="M4 11h16v2H4z" fill-rule="evenodd"/></svg>',
'huddle': '<svg height="24" width="24"><path d="M10.3 5.6V8h.8v-2l3.5 2 4.3-2.5L14.6 3l-4.3 2.6zM14.6 8v5.2l4.3-2.6V5.6L14.6 8zM6 13.3v2.5h.9v-2l3.4 2 4.3-2.5-4.3-2.6zm4.3 2.6V21l4.3-2.6v-5.1l-4.3 2.6z"/></svg>',
'image-decorative': '<svg width="24" height="24"><path d="M12 10c.3 0 .5-.1.7-.3A1 1 0 0 0 13 9a1 1 0 0 0-.3-.7A1 1 0 0 0 12 8a1 1 0 0 0-.7.3 1 1 0 0 0-.3.7 1 1 0 0 0 1 1Zm0 6a3 3 0 0 1-1.8-.5A3 3 0 0 1 9.1 14a2 2 0 0 0-.2 0 3 3 0 0 1-2.5-1 3 3 0 0 1-.9-2.2A3 3 0 0 1 6.2 9a3.4 3.4 0 0 1-.5-1 3 3 0 0 1-.2-1 3 3 0 0 1 1-2.2A3 3 0 0 1 8.8 4H9a3 3 0 0 1 1.1-1.4A3 3 0 0 1 12 2a3 3 0 0 1 1.8.5c.5.4.9.9 1.1 1.5h.2a3 3 0 0 1 2.5.8 3 3 0 0 1 .9 2.3c0 .3 0 .7-.2 1a2.8 2.8 0 0 1-.5.9l.5 1 .2 1a3 3 0 0 1-1 2.2 3 3 0 0 1-2.4.9 1.8 1.8 0 0 0-.2 0 3 3 0 0 1-1.1 1.4 3 3 0 0 1-1.8.5Zm0 7a8.7 8.7 0 0 1 .7-3.5 9.2 9.2 0 0 1 2-2.8 9.2 9.2 0 0 1 2.8-2A8.6 8.6 0 0 1 21 14a8.6 8.6 0 0 1-.7 3.5 9.2 9.2 0 0 1-2 2.8 9.2 9.2 0 0 1-2.8 2 8.6 8.6 0 0 1-3.5.7Zm2.5-2.5a6.8 6.8 0 0 0 2.4-1.5 6.8 6.8 0 0 0 1.7-2.6c-1 .4-1.8 1-2.6 1.7a6.8 6.8 0 0 0-1.5 2.4ZM12 23a8.6 8.6 0 0 0-.7-3.5 9.2 9.2 0 0 0-2-2.9 9.2 9.2 0 0 0-2.8-1.9A8.6 8.6 0 0 0 3 14a8.7 8.7 0 0 0 .7 3.5 9.2 9.2 0 0 0 2 2.9 9.2 9.2 0 0 0 2.8 1.9 8.6 8.6 0 0 0 3.5.7Zm-2.5-2.5A6.8 6.8 0 0 1 7.2 19a6.8 6.8 0 0 1-1.6-2.6c.9.4 1.7 1 2.5 1.7a6.8 6.8 0 0 1 1.6 2.4Zm5.9-8.4c.3 0 .5-.1.8-.4.2-.2.3-.5.3-.8 0-.2 0-.4-.2-.6a1.3 1.3 0 0 0-.5-.4l-.8-.4a3 3 0 0 1-.2.5 3.8 3.8 0 0 1-.2.5 2.5 2.5 0 0 1-.3.4 4 4 0 0 1-.4.4l.8.6a.8.8 0 0 0 .3.1h.4ZM15 8.5l.8-.4.5-.4.2-.6a1.1 1.1 0 0 0-.3-.8 1 1 0 0 0-.8-.4 1.1 1.1 0 0 0-.7.2l-.8.6.4.4.3.4a3.9 3.9 0 0 1 .4 1Zm-4-2.3a2.7 2.7 0 0 1 1-.2 2.7 2.7 0 0 1 1 .2l.2-1.1c0-.3-.1-.6-.4-.8A1.2 1.2 0 0 0 12 4c-.3 0-.6.1-.8.3-.3.2-.4.5-.3.8v1.1Zm1 7.8c.3 0 .6-.1.8-.3.3-.2.4-.5.3-.8v-1.1a2.7 2.7 0 0 1-1.1.2 2.7 2.7 0 0 1-1-.2l-.2 1.1c0 .3.1.6.4.8.2.2.5.3.8.3ZM9 8.5a3 3 0 0 1 .4-1l.3-.4.4-.4-.8-.6A.9.9 0 0 0 9 6a1.2 1.2 0 0 0-.3 0c-.4 0-.6 0-.9.3a1.1 1.1 0 0 0-.3.8c0 .2 0 .4.2.6l.5.4.9.4Zm-.3 3.6H9l.3-.2.8-.6a5.5 5.5 0 0 1-.4-.4 2.1 2.1 0 0 1-.3-.4 3.8 3.8 0 0 1-.2-.5 3 3 0 0 1-.1-.5l-1 .4-.4.4a1.1 1.1 0 0 0-.2.6c0 .3.2.6.4.8.2.2.4.4.7.4Z"/></svg>',
'image-enhancements': '<svg height="24" width="24"><path d="M5.3 21a2 2 0 0 1-1.5-.6 2 2 0 0 1-.6-1.4V5c0-.6.2-1 .6-1.4A2 2 0 0 1 5.2 3H13v2H5.2v14h14v-8h2v8c0 .6-.1 1-.5 1.4a2 2 0 0 1-1.4.6z"/><path d="M11 9a4 4 0 0 0 4 4 4 4 0 0 0-4 4 4 4 0 0 0-4-4 4 4 0 0 0 4-4zm5.5-6c.2 2.1 2.2 4.1 4.5 4.5-2.3.4-4.3 2.4-4.5 4.5A5.2 5.2 0 0 0 12 7.5 5.2 5.2 0 0 0 16.5 3z"/></svg>',
'image-options': '<svg width="24" height="24"><path d="M6 10a2 2 0 0 0-2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2 2 2 0 0 0-2-2Zm12 0a2 2 0 0 0-2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2 2 2 0 0 0-2-2Zm-6 0a2 2 0 0 0-2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2 2 2 0 0 0-2-2Z" fill-rule="nonzero"/></svg>',
'image': '<svg width="24" height="24"><path d="m5 15.7 3.3-3.2c.3-.3.7-.3 1 0L12 15l4.1-4c.3-.4.8-.4 1 0l2 1.9V5H5v10.7ZM5 18V19h3l2.8-2.9-2-2L5 17.9Zm14-3-2.5-2.4-6.4 6.5H19v-4ZM4 3h16c.6 0 1 .4 1 1v16c0 .6-.4 1-1 1H4a1 1 0 0 1-1-1V4c0-.6.4-1 1-1Zm6 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z" fill-rule="nonzero"/></svg>',
'import-word': '<svg width="24" height="24"><path fill-rule="evenodd" clip-rule="evenodd" d="M7 3h7.4L19 7.6V15h-2V9h-4V5H5c0-1.1.9-2 2-2Z"/><path d="M9.5 7A1.5 1.5 0 0 1 11 8.4v7.1A1.5 1.5 0 0 1 9.6 17H2.5A1.5 1.5 0 0 1 1 15.6V8.5A1.5 1.5 0 0 1 2.4 7h7.1Zm-1 2.8-1 2.6-1-2.5v-.1a.6.6 0 0 0-1 0l-.1.1-.9 2.5-1-2.5v-.1a.6.6 0 0 0-1 .4v.1l1.5 4v.1a.6.6 0 0 0 1 0v-.1l1-2.5.9 2.5v.1a.6.6 0 0 0 1 0H8l1.6-4v-.2a.6.6 0 0 0-1.1-.4Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M11.4 18.2a1 1 0 0 0 1.2 1.6l1.4-1V22a1 1 0 1 0 2 0v-3.1l1.4 1a1 1 0 0 0 1.2-1.7L15 15.8l-3.6 2.4Z"/></svg>',
'improve-writing': '<svg height="24" width="24"><path d="M14.8 4.1c-1 0-2.2.5-3 1.3L6.5 11a.8.8 0 0 0-.2.6V16l7.9-8.3.7.8.6.7-4.4 4.6h4.7l2-2.2a4.5 4.5 0 0 0 1-1.7l2 .6a6.5 6.5 0 0 1-1.6 2.5L14 18.7a2.7 2.7 0 0 1-1 .7 2.6 2.6 0 0 1-1 .2H5.8L3.4 22 2 20.5l2.4-2.4a2 2 0 0 1 0-.5v-6c0-.8.2-1.5.7-2L10.5 4A6 6 0 0 1 14.8 2v2zM7.6 17.5H12a.8.8 0 0 0 .5-.2l1.3-1.4H9.2z" fill-rule="evenodd"/><path d="M18.8 2.2a.3.3 0 0 1 .6 0 4 4 0 0 0 2.4 2.5c.3.1.3.5 0 .7h-.2a4.1 4.1 0 0 0-2.2 2.4.3.3 0 0 1-.6 0 4.1 4.1 0 0 0-2.2-2.4h-.2c-.3-.2-.3-.6 0-.7 1.1-.4 2-1.3 2.4-2.5z"/></svg>',
'indent': '<svg width="24" height="24"><path d="M7 5h12c.6 0 1 .4 1 1s-.4 1-1 1H7a1 1 0 1 1 0-2Zm5 4h7c.6 0 1 .4 1 1s-.4 1-1 1h-7a1 1 0 0 1 0-2Zm0 4h7c.6 0 1 .4 1 1s-.4 1-1 1h-7a1 1 0 0 1 0-2Zm-5 4h12a1 1 0 0 1 0 2H7a1 1 0 0 1 0-2Zm-2.6-3.8L6.2 12l-1.8-1.2a1 1 0 0 1 1.2-1.6l3 2a1 1 0 0 1 0 1.6l-3 2a1 1 0 1 1-1.2-1.6Z" fill-rule="evenodd"/></svg>',
'info': '<svg width="24" height="24"><path d="M12 4a7.8 7.8 0 0 1 5.7 2.3A8 8 0 1 1 12 4Zm-1 3v2h2V7h-2Zm3 10v-1h-1v-5h-3v1h1v4h-1v1h4Z" fill-rule="evenodd"/></svg>',
'insert-character': '<svg width="24" height="24"><path d="M15 18h4l1-2v4h-6v-3.3l1.4-1a6 6 0 0 0 1.8-2.9 6.3 6.3 0 0 0-.1-4.1 5.8 5.8 0 0 0-3-3.2c-.6-.3-1.3-.5-2.1-.5a5.1 5.1 0 0 0-3.9 1.8 6.3 6.3 0 0 0-1.3 6 6.2 6.2 0 0 0 1.8 3l1.4.9V20H4v-4l1 2h4v-.5l-2-1L5.4 15A6.5 6.5 0 0 1 4 11c0-1 .2-1.9.6-2.7A7 7 0 0 1 6.3 6C7.1 5.4 8 5 9 4.5c1-.3 2-.5 3.1-.5a8.8 8.8 0 0 1 5.7 2 7 7 0 0 1 1.7 2.3 6 6 0 0 1 .2 4.8c-.2.7-.6 1.3-1 1.9a7.6 7.6 0 0 1-3.6 2.5v.5Z" fill-rule="evenodd"/></svg>',
'insert-time': '<svg width="24" height="24"><g fill-rule="nonzero"><path d="M12 19a7 7 0 1 0 0-14 7 7 0 0 0 0 14Zm0 2a9 9 0 1 1 0-18 9 9 0 0 1 0 18Z"/><path d="M16 12h-3V7c0-.6-.4-1-1-1a1 1 0 0 0-1 1v7h5c.6 0 1-.4 1-1s-.4-1-1-1Z"/></g></svg>',
'instagram': '<svg height="24" width="24"><path clip-rule="evenodd" d="M10.1 9a4 4 0 1 1 4.4 6.6A4 4 0 0 1 10.1 9zm2.2 1.3a2 2 0 1 0 0 4 2 2 0 0 0 0-4z" fill-rule="evenodd"/><path d="M16 9.3a1.3 1.3 0 1 0 0-2.6 1.3 1.3 0 0 0 0 2.6z"/><path clip-rule="evenodd" d="M3 6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3zm3-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1z" fill-rule="evenodd"/></svg>',
'invert': '<svg height="24" width="24"><path d="M5 4 3.6 5.4l3.9 4C6.6 10.7 6 12.3 6 14a6 6 0 0 0 10.3 4.2l1.9 1.8 1.4-1.4z" fill-rule="evenodd"/><path d="M13.4 8.4c.9 1 1.6 2.1 2 3.2l2.6 2.6V14c0-5-6-10-6-10s-1 .8-2.1 2l1.4 1.5.7-.7 1.4 1.6z"/></svg>',
'italic': '<svg width="24" height="24"><path d="m16.7 4.7-.1.9h-.3c-.6 0-1 0-1.4.3-.3.3-.4.6-.5 1.1l-2.1 9.8v.6c0 .5.4.8 1.4.8h.2l-.2.8H8l.2-.8h.2c1.1 0 1.8-.5 2-1.5l2-9.8.1-.5c0-.6-.4-.8-1.4-.8h-.3l.2-.9h5.8Z" fill-rule="evenodd"/></svg>',
'language': '<svg width="24" height="24"><path d="M12 3a9 9 0 1 1 0 18 9 9 0 0 1 0-18Zm4.3 13.3c-.5 1-1.2 2-2 2.9a7.5 7.5 0 0 0 3.2-2.1l-.2-.2a6 6 0 0 0-1-.6Zm-8.6 0c-.5.2-.9.5-1.2.8.9 1 2 1.7 3.2 2a10 10 0 0 1-2-2.8Zm3.6-.8c-.8 0-1.6.1-2.2.3.5 1 1.2 1.9 2.1 2.7Zm1.5 0v3c.9-.8 1.6-1.7 2.1-2.7-.6-.2-1.4-.3-2.1-.3Zm-6-2.7H4.5c.2 1 .5 2.1 1 3h.3l1.3-1a10 10 0 0 1-.3-2Zm12.7 0h-2.3c0 .7-.1 1.4-.3 2l1.6 1.1c.5-1 .9-2 1-3.1Zm-3.8 0h-3V14c1 0 2 .1 2.7.4.2-.5.3-1 .3-1.6Zm-4.4 0h-3l.3 1.6c.8-.3 1.7-.4 2.7-.4v-1.3Zm-5.5-5c-.7 1-1.1 2.2-1.3 3.5h2.3c0-1 .2-1.8.5-2.6l-1.5-1Zm2.9 1.4v.1c-.2.6-.4 1.3-.4 2h3V9.4c-1 0-1.8-.1-2.6-.3Zm6.6 0h-.1l-2.4.3v1.8h3l-.5-2.1Zm3-1.4-.3.1-1.3.8c.3.8.5 1.6.5 2.6h2.3a7.5 7.5 0 0 0-1.3-3.5Zm-9 0 2 .2V5.5a9 9 0 0 0-2 2.2Zm3.5-2.3V8c.6 0 1.3 0 1.9-.2a9 9 0 0 0-2-2.3Zm-3-.7h-.1c-1.1.4-2.1 1-3 1.8l1.2.7a10 10 0 0 1 1.9-2.5Zm4.4 0 .1.1a10 10 0 0 1 1.8 2.4l1.1-.7a7.5 7.5 0 0 0-3-1.8Z"/></svg>',
'line-height': '<svg width="24" height="24"><path d="M21 5a1 1 0 0 1 .1 2H13a1 1 0 0 1-.1-2H21zm0 4a1 1 0 0 1 .1 2H13a1 1 0 0 1-.1-2H21zm0 4a1 1 0 0 1 .1 2H13a1 1 0 0 1-.1-2H21zm0 4a1 1 0 0 1 .1 2H13a1 1 0 0 1-.1-2H21zM7 3.6l3.7 3.7a1 1 0 0 1-1.3 1.5h-.1L8 7.3v9.2l1.3-1.3a1 1 0 0 1 1.3 0h.1c.4.4.4 1 0 1.3v.1L7 20.4l-3.7-3.7a1 1 0 0 1 1.3-1.5h.1L6 16.7V7.4L4.7 8.7a1 1 0 0 1-1.3 0h-.1a1 1 0 0 1 0-1.3v-.1L7 3.6z"/></svg>',
'line': '<svg width="24" height="24"><path d="m15 9-8 8H4v-3l8-8 3 3Zm1-1-3-3 1-1h1c-.2 0 0 0 0 0l2 2s0 .2 0 0v1l-1 1ZM4 18h16v2H4v-2Z" fill-rule="evenodd"/></svg>',
'link': '<svg width="24" height="24"><path d="M6.2 12.3a1 1 0 0 1 1.4 1.4l-2 2a2 2 0 1 0 2.6 2.8l4.8-4.8a1 1 0 0 0 0-1.4 1 1 0 1 1 1.4-1.3 2.9 2.9 0 0 1 0 4L9.6 20a3.9 3.9 0 0 1-5.5-5.5l2-2Zm11.6-.6a1 1 0 0 1-1.4-1.4l2-2a2 2 0 1 0-2.6-2.8L11 10.3a1 1 0 0 0 0 1.4A1 1 0 1 1 9.6 13a2.9 2.9 0 0 1 0-4L14.4 4a3.9 3.9 0 0 1 5.5 5.5l-2 2Z" fill-rule="nonzero"/></svg>',
'list-bull-circle': '<svg height="48" width="48"><path d="M8 14a3 3 0 1 0 6 0 3 3 0 0 0-6 0zm5 0a2 2 0 1 1-4 0 2 2 0 0 1 4 0zM8 24a3 3 0 1 0 6 0 3 3 0 0 0-6 0zm5 0a2 2 0 1 1-4 0 2 2 0 0 1 4 0zM8 34a3 3 0 1 0 6 0 3 3 0 0 0-6 0zm5 0a2 2 0 1 1-4 0 2 2 0 0 1 4 0z" fill-rule="evenodd"/><path d="M18 12h22v4H18zm0 10h22v4H18zm0 10h22v4H18z" opacity=".2"/></svg>',
'list-bull-default': '<svg height="48" width="48"><path d="M14 14a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/><path d="M12 24a3 3 0 1 0 6 0 3 3 0 0 0-6 0zm5 0a2 2 0 1 1-4 0 2 2 0 0 1 4 0z" fill-rule="evenodd"/><path d="M16 31h6v6h-6z"/><path d="M16 31h6v6h-6z"/><path d="M18 12h22v4H18zm4 10h18v4H22zm4 10h14v4H26z" opacity=".2"/></svg>',
'list-bull-disc': '<svg height="48" width="48"><path d="M14 14a3 3 0 1 1-6 0 3 3 0 0 1 6 0zm0 10a3 3 0 1 1-6 0 3 3 0 0 1 6 0zm0 10a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/><path d="M18 12h22v4H18zm0 10h22v4H18zm0 10h22v4H18z" opacity=".2"/></svg>',
'list-bull-square': '<svg height="48" width="48"><path d="M8 21h6v6H8zm0 10h6v6H8zm0-20h6v6H8z"/><path d="M18 12h22v4H18zm0 10h22v4H18zm0 10h22v4H18z" opacity=".2"/></svg>',
'list-num-default-rtl': '<svg width="48" height="48"><g fill-rule="evenodd"><path opacity=".2" d="M8 12h22v4H8zM8 22h22v4H8zM8 32h22v4H8z"/><path d="M37.4 17v-4.8h-.1l-1.5 1v-1.1l1.6-1.1h1.2v6zM33.3 17.1c-.5 0-.8-.3-.8-.7 0-.4.3-.7.8-.7.4 0 .7.3.7.7 0 .4-.3.7-.7.7zm1.7 5.7c0-1.2 1-2 2.2-2 1.3 0 2.1.8 2.1 1.8 0 .7-.3 1.2-1.3 2.2l-1.2 1v.2h2.6v1h-4.3v-.9l2-1.9c.8-.8 1-1.1 1-1.5 0-.5-.4-.8-1-.8-.5 0-.9.3-.9.9H35zm-1.7 4.3c-.5 0-.8-.3-.8-.7 0-.4.3-.7.8-.7.4 0 .7.3.7.7 0 .4-.3.7-.7.7zm3.2 7.3v-1h.7c.6 0 1-.3 1-.8 0-.4-.4-.7-1-.7s-1 .3-1 .8H35c0-1.1 1-1.8 2.2-1.8 1.2 0 2.1.6 2.1 1.6 0 .7-.4 1.2-1 1.3v.1c.7.1 1.3.7 1.3 1.4 0 1-1 1.9-2.4 1.9-1.3 0-2.2-.8-2.3-2h1.2c0 .6.5 1 1.1 1 .6 0 1-.4 1-1 0-.5-.3-.8-1-.8h-.7zm-3.3 2.7c-.4 0-.7-.3-.7-.7 0-.4.3-.7.7-.7.5 0 .8.3.8.7 0 .4-.3.7-.8.7z"/></g></svg>',
'list-num-default': '<svg width="48" height="48"><g fill-rule="evenodd"><path opacity=".2" d="M18 12h22v4H18zM18 22h22v4H18zM18 32h22v4H18z"/><path d="M10 17v-4.8l-1.5 1v-1.1l1.6-1h1.2V17h-1.2Zm3.6.1c-.4 0-.7-.3-.7-.7 0-.4.3-.7.7-.7.5 0 .7.3.7.7 0 .4-.2.7-.7.7Zm-5 5.7c0-1.2.8-2 2.1-2s2.1.8 2.1 1.8c0 .7-.3 1.2-1.4 2.2l-1.1 1v.2h2.6v1H8.6v-.9l2-1.9c.8-.8 1-1.1 1-1.5 0-.5-.4-.8-1-.8-.5 0-.9.3-.9.9H8.5Zm6.3 4.3c-.5 0-.7-.3-.7-.7 0-.4.2-.7.7-.7.4 0 .7.3.7.7 0 .4-.3.7-.7.7ZM10 34.4v-1h.7c.6 0 1-.3 1-.8 0-.4-.4-.7-1-.7s-1 .3-1 .8H8.6c0-1.1 1-1.8 2.2-1.8 1.3 0 2.1.6 2.1 1.6 0 .7-.4 1.2-1 1.3v.1c.8.1 1.3.7 1.3 1.4 0 1-1 1.9-2.4 1.9-1.3 0-2.2-.8-2.3-2h1.2c0 .6.5 1 1.1 1 .7 0 1-.4 1-1 0-.5-.3-.8-1-.8h-.7Zm4.7 2.7c-.4 0-.7-.3-.7-.7 0-.4.3-.7.7-.7.5 0 .8.3.8.7 0 .4-.3.7-.8.7Z"/></g></svg>',
'list-num-lower-alpha-rtl': '<svg width="48" height="48"><g fill-rule="evenodd"><path opacity=".2" d="M8 12h22v4H8zM8 22h22v4H8zM8 32h22v4H8z"/><path d="M36.5 16c-.9 0-1.5-.5-1.5-1.3s.6-1.3 1.8-1.4h1v-.4c0-.4-.2-.6-.7-.6-.4 0-.7.1-.8.4h-1.1c0-.8.8-1.4 2-1.4S39 12 39 13V16h-1.2v-.6c-.3.4-.8.7-1.4.7Zm.4-.8c.6 0 1-.4 1-.9V14h-1c-.5.1-.7.3-.7.6 0 .4.3.6.7.6ZM33.1 16.1c-.4 0-.7-.3-.7-.7 0-.4.3-.7.7-.7.5 0 .8.3.8.7 0 .4-.3.7-.8.7ZM37.7 26c-.7 0-1.2-.2-1.5-.7v.7H35v-6.3h1.2v2.5c.3-.5.8-.9 1.5-.9 1.1 0 1.8 1 1.8 2.4 0 1.5-.7 2.4-1.8 2.4Zm-.5-3.6c-.6 0-1 .5-1 1.3s.4 1.4 1 1.4c.7 0 1-.6 1-1.4 0-.8-.3-1.3-1-1.3ZM33.2 26.1c-.4 0-.7-.3-.7-.7 0-.4.3-.7.7-.7.5 0 .8.3.8.7 0 .4-.3.7-.8.7zm6 7h-1c-.1-.5-.4-.8-1-.8s-1 .5-1 1.4c0 1 .4 1.4 1 1.4.5 0 .9-.2 1-.7h1c0 1-.8 1.7-2 1.7-1.4 0-2.2-.9-2.2-2.4s.8-2.4 2.2-2.4c1.2 0 2 .7 2 1.7zm-6.1 3c-.5 0-.7-.3-.7-.7 0-.4.2-.7.7-.7.4 0 .7.3.7.7 0 .4-.3.7-.7.7z"/></g></svg>',
'list-num-lower-alpha': '<svg width="48" height="48"><g fill-rule="evenodd"><path opacity=".2" d="M18 12h22v4H18zM18 22h22v4H18zM18 32h22v4H18z"/><path d="M10.3 15.2c.5 0 1-.4 1-.9V14h-1c-.5.1-.8.3-.8.6 0 .4.3.6.8.6Zm-.4.9c-1 0-1.5-.6-1.5-1.4 0-.8.6-1.3 1.7-1.4h1.1v-.4c0-.4-.2-.6-.7-.6-.5 0-.8.1-.9.4h-1c0-.8.8-1.4 2-1.4 1.1 0 1.8.6 1.8 1.6V16h-1.1v-.6h-.1c-.2.4-.7.7-1.3.7Zm4.6 0c-.5 0-.7-.3-.7-.7 0-.4.2-.7.7-.7.4 0 .7.3.7.7 0 .4-.3.7-.7.7Zm-3.2 10c-.6 0-1.2-.3-1.4-.8v.7H8.5v-6.3H10v2.5c.3-.5.8-.9 1.4-.9 1.2 0 1.9 1 1.9 2.4 0 1.5-.7 2.4-1.9 2.4Zm-.4-3.7c-.7 0-1 .5-1 1.3s.3 1.4 1 1.4c.6 0 1-.6 1-1.4 0-.8-.4-1.3-1-1.3Zm4 3.7c-.5 0-.7-.3-.7-.7 0-.4.2-.7.7-.7.4 0 .7.3.7.7 0 .4-.3.7-.7.7Zm-2.2 7h-1.2c0-.5-.4-.8-.9-.8-.6 0-1 .5-1 1.4 0 1 .4 1.4 1 1.4.5 0 .8-.2 1-.7h1c0 1-.8 1.7-2 1.7-1.4 0-2.2-.9-2.2-2.4s.8-2.4 2.2-2.4c1.2 0 2 .7 2 1.7Zm1.8 3c-.5 0-.8-.3-.8-.7 0-.4.3-.7.8-.7.4 0 .7.3.7.7 0 .4-.3.7-.7.7Z"/></g></svg>',
'list-num-lower-greek-rtl': '<svg width="48" height="48"><g fill-rule="evenodd"><path opacity=".2" d="M8 12h22v4H8zM8 22h22v4H8zM8 32h22v4H8z"/><path d="M37.4 16c-1.2 0-2-.8-2-2.3 0-1.5.8-2.4 2-2.4.6 0 1 .4 1.3 1v-.9H40v3.2c0 .4.1.5.4.5h.2v.9h-.6c-.6 0-1-.2-1-.7h-.2c-.2.4-.7.8-1.3.8Zm.3-1c.6 0 1-.5 1-1.3s-.4-1.3-1-1.3-1 .5-1 1.3.4 1.4 1 1.4ZM33.3 16.1c-.5 0-.8-.3-.8-.7 0-.4.3-.7.8-.7.4 0 .7.3.7.7 0 .4-.3.7-.7.7ZM36 21.9c0-1.5.8-2.3 2.1-2.3 1.2 0 2 .6 2 1.6 0 .6-.3 1-.9 1.3.9.3 1.3.8 1.3 1.7 0 1.2-.7 1.9-1.8 1.9-.6 0-1.1-.3-1.4-.8v2.2H36V22Zm1.8 1.2v-1h.3c.5 0 .9-.2.9-.7 0-.5-.3-.8-.9-.8-.5 0-.8.3-.8 1v2.2c0 .8.4 1.3 1 1.3s1-.4 1-1-.4-1-1.2-1h-.3ZM33.3 26.1c-.5 0-.8-.3-.8-.7 0-.4.3-.7.8-.7.4 0 .7.3.7.7 0 .4-.3.7-.7.7ZM37.1 34.6 34.8 30h1.4l1.7 3.5 1.7-3.5h1.1l-2.2 4.6v.1c.5.8.7 1.4.7 1.8 0 .4-.2.8-.4 1-.2.2-.6.3-1 .3-.9 0-1.3-.4-1.3-1.2 0-.5.2-1 .5-1.7l.1-.2Zm.7 1a2 2 0 0 0-.4.9c0 .3.1.4.4.4.3 0 .4-.1.4-.4 0-.2-.1-.6-.4-1ZM33.3 36.1c-.5 0-.8-.3-.8-.7 0-.4.3-.7.8-.7.4 0 .7.3.7.7 0 .4-.3.7-.7.7Z"/></g></svg>',
'list-num-lower-greek': '<svg width="48" height="48"><g fill-rule="evenodd"><path opacity=".2" d="M18 12h22v4H18zM18 22h22v4H18zM18 32h22v4H18z"/><path d="M10.5 15c.7 0 1-.5 1-1.3s-.3-1.3-1-1.3c-.5 0-.9.5-.9 1.3s.4 1.4 1 1.4Zm-.3 1c-1.1 0-1.8-.8-1.8-2.3 0-1.5.7-2.4 1.8-2.4.7 0 1.1.4 1.3 1h.1v-.9h1.2v3.2c0 .4.1.5.4.5h.2v.9h-.6c-.6 0-1-.2-1.1-.7h-.1c-.2.4-.7.8-1.4.8Zm5 .1c-.5 0-.8-.3-.8-.7 0-.4.3-.7.7-.7.5 0 .8.3.8.7 0 .4-.3.7-.8.7Zm-4.9 7v-1h.3c.6 0 1-.2 1-.7 0-.5-.4-.8-1-.8-.5 0-.8.3-.8 1v2.2c0 .8.4 1.3 1.1 1.3.6 0 1-.4 1-1s-.5-1-1.3-1h-.3ZM8.6 22c0-1.5.7-2.3 2-2.3 1.2 0 2 .6 2 1.6 0 .6-.3 1-.8 1.3.8.3 1.3.8 1.3 1.7 0 1.2-.8 1.9-1.9 1.9-.6 0-1.1-.3-1.3-.8v2.2H8.5V22Zm6.2 4.2c-.4 0-.7-.3-.7-.7 0-.4.3-.7.7-.7.5 0 .7.3.7.7 0 .4-.2.7-.7.7Zm-4.5 8.5L8 30h1.4l1.7 3.5 1.7-3.5h1.1l-2.2 4.6v.1c.5.8.7 1.4.7 1.8 0 .4-.1.8-.4 1-.2.2-.6.3-1 .3-.9 0-1.3-.4-1.3-1.2 0-.5.2-1 .5-1.7l.1-.2Zm.7 1a2 2 0 0 0-.4.9c0 .3.1.4.4.4.3 0 .4-.1.4-.4 0-.2-.1-.6-.4-1Zm4.5.5c-.5 0-.8-.3-.8-.7 0-.4.3-.7.8-.7.4 0 .7.3.7.7 0 .4-.3.7-.7.7Z"/></g></svg>',
'list-num-lower-roman-rtl': '<svg width="48" height="48"><g fill-rule="evenodd"><path opacity=".2" d="M8 12h22v4H8zM8 22h22v4H8zM8 32h22v4H8z"/><path d="M32.9 16v-1.2h-1.3V16H33Zm0 10v-1.2h-1.3V26H33Zm0 10v-1.2h-1.3V36H33Z"/><path fill-rule="nonzero" d="M36 21h-1.5v5H36zM36 31h-1.5v5H36zM39 21h-1.5v5H39zM39 31h-1.5v5H39zM42 31h-1.5v5H42zM36 11h-1.5v5H36zM36 19h-1.5v1H36zM36 29h-1.5v1H36zM39 19h-1.5v1H39zM39 29h-1.5v1H39zM42 29h-1.5v1H42zM36 9h-1.5v1H36z"/></g></svg>',
'list-num-lower-roman': '<svg width="48" height="48"><g fill-rule="evenodd"><path opacity=".2" d="M18 12h22v4H18zM18 22h22v4H18zM18 32h22v4H18z"/><path d="M15.1 16v-1.2h1.3V16H15Zm0 10v-1.2h1.3V26H15Zm0 10v-1.2h1.3V36H15Z"/><path fill-rule="nonzero" d="M12 21h1.5v5H12zM12 31h1.5v5H12zM9 21h1.5v5H9zM9 31h1.5v5H9zM6 31h1.5v5H6zM12 11h1.5v5H12zM12 19h1.5v1H12zM12 29h1.5v1H12zM9 19h1.5v1H9zM9 29h1.5v1H9zM6 29h1.5v1H6zM12 9h1.5v1H12z"/></g></svg>',
'list-num-upper-alpha-rtl': '<svg width="48" height="48"><g fill-rule="evenodd"><path opacity=".2" d="M8 12h22v4H8zM8 22h22v4H8zM8 32h22v4H8z"/><path d="m39.3 17-.5-1.4h-2l-.5 1.4H35l2-6h1.6l2 6h-1.3Zm-1.6-4.7-.7 2.3h1.6l-.8-2.3ZM33.4 17c-.4 0-.7-.3-.7-.7 0-.4.3-.7.7-.7.5 0 .7.3.7.7 0 .4-.2.7-.7.7Zm4.7 9.9h-2.7v-6H38c1.2 0 1.9.6 1.9 1.5 0 .6-.5 1.2-1 1.3.7.1 1.3.7 1.3 1.5 0 1-.8 1.7-2 1.7Zm-1.4-5v1.5h1c.6 0 1-.3 1-.8 0-.4-.4-.7-1-.7h-1Zm0 4h1.1c.7 0 1.1-.3 1.1-.8 0-.6-.4-.9-1.1-.9h-1.1V26ZM33 27.1c-.5 0-.8-.3-.8-.7 0-.4.3-.7.8-.7.4 0 .7.3.7.7 0 .4-.3.7-.7.7Zm4.9 10c-1.8 0-2.8-1.1-2.8-3.1s1-3.1 2.8-3.1c1.4 0 2.5.9 2.6 2.2h-1.3c0-.7-.6-1.1-1.3-1.1-1 0-1.6.7-1.6 2s.6 2 1.6 2c.7 0 1.2-.4 1.4-1h1.2c-.1 1.3-1.2 2.2-2.6 2.2Zm-4.5 0c-.5 0-.8-.3-.8-.7 0-.4.3-.7.8-.7.4 0 .7.3.7.7 0 .4-.3.7-.7.7Z"/></g></svg>',
'list-num-upper-alpha': '<svg width="48" height="48"><g fill-rule="evenodd"><path opacity=".2" d="M18 12h22v4H18zM18 22h22v4H18zM18 32h22v4H18z"/><path d="m12.6 17-.5-1.4h-2L9.5 17H8.3l2-6H12l2 6h-1.3ZM11 12.3l-.7 2.3h1.6l-.8-2.3Zm4.7 4.8c-.4 0-.7-.3-.7-.7 0-.4.3-.7.7-.7.5 0 .7.3.7.7 0 .4-.2.7-.7.7ZM11.4 27H8.7v-6h2.6c1.2 0 1.9.6 1.9 1.5 0 .6-.5 1.2-1 1.3.7.1 1.3.7 1.3 1.5 0 1-.8 1.7-2 1.7ZM10 22v1.5h1c.6 0 1-.3 1-.8 0-.4-.4-.7-1-.7h-1Zm0 4H11c.7 0 1.1-.3 1.1-.8 0-.6-.4-.9-1.1-.9H10V26Zm5.4 1.1c-.5 0-.8-.3-.8-.7 0-.4.3-.7.8-.7.4 0 .7.3.7.7 0 .4-.3.7-.7.7Zm-4.1 10c-1.8 0-2.8-1.1-2.8-3.1s1-3.1 2.8-3.1c1.4 0 2.5.9 2.6 2.2h-1.3c0-.7-.6-1.1-1.3-1.1-1 0-1.6.7-1.6 2s.6 2 1.6 2c.7 0 1.2-.4 1.4-1h1.2c-.1 1.3-1.2 2.2-2.6 2.2Zm4.5 0c-.5 0-.8-.3-.8-.7 0-.4.3-.7.8-.7.4 0 .7.3.7.7 0 .4-.3.7-.7.7Z"/></g></svg>',
'list-num-upper-roman-rtl': '<svg width="48" height="48"><g fill-rule="evenodd"><path opacity=".2" d="M8 12h22v4H8zM8 22h22v4H8zM8 32h22v4H8z"/><path d="M31.6 17v-1.2H33V17h-1.3Zm0 10v-1.2H33V27h-1.3Zm0 10v-1.2H33V37h-1.3Z"/><path fill-rule="nonzero" d="M34.5 20H36v7h-1.5zM34.5 30H36v7h-1.5zM37.5 20H39v7h-1.5zM37.5 30H39v7h-1.5zM40.5 30H42v7h-1.5zM34.5 10H36v7h-1.5z"/></g></svg>',
'list-num-upper-roman': '<svg width="48" height="48"><g fill-rule="evenodd"><path opacity=".2" d="M18 12h22v4H18zM18 22h22v4H18zM18 32h22v4H18z"/><path d="M15.1 17v-1.2h1.3V17H15Zm0 10v-1.2h1.3V27H15Zm0 10v-1.2h1.3V37H15Z"/><path fill-rule="nonzero" d="M12 20h1.5v7H12zM12 30h1.5v7H12zM9 20h1.5v7H9zM9 30h1.5v7H9zM6 30h1.5v7H6zM12 10h1.5v7H12z"/></g></svg>',
'lock': '<svg width="24" height="24"><path d="M16.3 11c.2 0 .3 0 .5.2l.2.6v7.4c0 .3 0 .4-.2.6l-.6.2H7.8c-.3 0-.4 0-.6-.2a.7.7 0 0 1-.2-.6v-7.4c0-.3 0-.4.2-.6l.5-.2H8V8c0-.8.3-1.5.9-2.1.6-.6 1.3-.9 2.1-.9h2c.8 0 1.5.3 2.1.9.6.6.9 1.3.9 2.1v3h.3ZM10 8v3h4V8a1 1 0 0 0-.3-.7A1 1 0 0 0 13 7h-2a1 1 0 0 0-.7.3 1 1 0 0 0-.3.7Z" fill-rule="evenodd"/></svg>',
'ltr': '<svg width="24" height="24"><path d="M11 5h7a1 1 0 0 1 0 2h-1v11a1 1 0 0 1-2 0V7h-2v11a1 1 0 0 1-2 0v-6c-.5 0-1 0-1.4-.3A3.4 3.4 0 0 1 7.8 10a3.3 3.3 0 0 1 0-2.8 3.4 3.4 0 0 1 1.8-1.8L11 5ZM4.4 16.2 6.2 15l-1.8-1.2a1 1 0 0 1 1.2-1.6l3 2a1 1 0 0 1 0 1.6l-3 2a1 1 0 1 1-1.2-1.6Z" fill-rule="evenodd"/></svg>',
'math-equation': '<svg width="24" height="24"><path fill-rule="evenodd" clip-rule="evenodd" d="M9 4.8c.1-.5.5-.8 1-.8h10a1 1 0 1 1 0 2h-9.2L8.3 19.2a1 1 0 0 1-1.7.4l-3.4-4.2a1 1 0 0 1 1.6-1.2l2 2.5L9 4.8Zm9.7 5.5c.4.4.4 1 0 1.4L17 13.5l1.8 1.8a1 1 0 1 1-1.4 1.4L15.5 15l-1.8 1.8a1 1 0 0 1-1.4-1.4l1.8-1.8-1.8-1.8a1 1 0 0 1 1.4-1.4l1.8 1.8 1.8-1.8a1 1 0 0 1 1.4 0Z"/></svg>',
'mentions': '<svg height="24" width="24"><path d="M12 21a8.8 8.8 0 0 1-3.5-.7 9 9 0 0 1-2.9-2 9 9 0 0 1-1.9-2.8A8.8 8.8 0 0 1 3 12c0-1.3.2-2.4.7-3.5a9 9 0 0 1 4.8-4.8A8.8 8.8 0 0 1 12 3c1.3 0 2.4.2 3.5.7a9.1 9.1 0 0 1 4.8 4.8A8.7 8.7 0 0 1 21 12v1.4a3 3 0 0 1-.9 2.2 3 3 0 0 1-2.2.9c-.5 0-1-.1-1.5-.4a3.8 3.8 0 0 1-1.1-1 4.8 4.8 0 0 1-1.5 1 4.3 4.3 0 0 1-1.8.4c-1.2 0-2.3-.4-3.2-1.3-.9-.9-1.3-2-1.3-3.2s.4-2.3 1.3-3.2c.9-.9 2-1.3 3.2-1.3s2.3.4 3.2 1.3c.9.9 1.3 2 1.3 3.2v1.4c0 .4.1.7.4 1 .3.3.6.4 1 .4s.7-.1 1-.4c.3-.3.4-.6.4-1V12c0-2-.7-3.8-2.1-5.2S14 4.7 12 4.7s-3.8.7-5.2 2.1S4.7 10 4.7 12s.7 3.8 2.1 5.2 3.2 2.1 5.2 2.1h4.5V21zm0-6.2c.8 0 1.4-.3 2-.8a2.7 2.7 0 0 0 .8-2c0-.8-.3-1.4-.8-2a2.7 2.7 0 0 0-2-.8c-.8 0-1.4.3-2 .8a2.7 2.7 0 0 0-.8 2c0 .8.3 1.4.8 2a2.7 2.7 0 0 0 2 .8z"/></svg>',
'minus': '<svg width="24" height="24"><path d="M19 11a1 1 0 0 1 .1 2H5a1 1 0 0 1-.1-2H19Z"/></svg>',
'more-drawer': '<svg width="24" height="24"><path d="M6 10a2 2 0 0 0-2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2 2 2 0 0 0-2-2Zm12 0a2 2 0 0 0-2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2 2 2 0 0 0-2-2Zm-6 0a2 2 0 0 0-2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2 2 2 0 0 0-2-2Z" fill-rule="nonzero"/></svg>',
'new-chat': '<svg height="24" width="24"><path d="M12 5H6v13.7h12v-7.9h2v7.9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6z"/><path d="M17 3.3a1 1 0 0 1 1.3 0l1.4 1.3a1 1 0 0 1 0 1.4l-5.2 5a1 1 0 0 1-.5.3l-3 .6.5-3a1 1 0 0 1 .3-.5L17 3.3z"/></svg>',
'new-document': '<svg width="24" height="24"><path d="M14.4 3H7a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h10a2 2 0 0 0 2-2V7.6L14.4 3ZM17 19H7V5h6v4h4v10Z" fill-rule="nonzero"/></svg>',
'new-tab': '<svg width="24" height="24"><path d="m15 13 2-2v8H5V7h8l-2 2H7v8h8v-4Zm4-8v5.5l-2-2-5.6 5.5H10v-1.4L15.5 7l-2-2H19Z" fill-rule="evenodd"/></svg>',
'non-breaking': '<svg width="24" height="24"><path d="M11 11H8a1 1 0 1 1 0-2h3V6c0-.6.4-1 1-1s1 .4 1 1v3h3c.6 0 1 .4 1 1s-.4 1-1 1h-3v3c0 .6-.4 1-1 1a1 1 0 0 1-1-1v-3Zm10 4v5H3v-5c0-.6.4-1 1-1s1 .4 1 1v3h14v-3c0-.6.4-1 1-1s1 .4 1 1Z" fill-rule="evenodd"/></svg>',
'notice': '<svg width="24" height="24"><path d="M15.5 4 20 8.5v7L15.5 20h-7L4 15.5v-7L8.5 4h7ZM13 17v-2h-2v2h2Zm0-4V7h-2v6h2Z" fill-rule="evenodd" clip-rule="evenodd"/></svg>',
'onedrive': '<svg height="24" width="24"><path d="M4 13.7h1zm16 .7h-1zm-11.7-4A4.4 4.4 0 0 1 12.1 8V6a6.4 6.4 0 0 0-5.6 3.4l1.8 1zM12.1 8c2 0 3.6 1.4 4 2.8l2-.5A6.3 6.3 0 0 0 12 6v2zm8.1 6.8-9.8-5.5-1 1.8 9.8 5.5 1-1.8zm-9.8-5.5A5 5 0 0 0 3 13.7h2A3 3 0 0 1 9.5 11l1-1.8zM3 13.7a5 5 0 0 0 1 2.8l1.5-1.1a3 3 0 0 1-.5-1.7zm15.8 1.6a2.3 2.3 0 0 1-2 1.4v2a4.3 4.3 0 0 0 3.9-2.6zm-2 1.4H8v2h8.7zm-8.8 0a3 3 0 0 1-2.5-1.3L4 16.5a5 5 0 0 0 4 2.2v-2zm12.7-.6c.2-.6.3-1.1.3-1.7h-2c0 .3 0 .6-.2.9zm.3-1.7c0-3-3.1-5.1-6-4l.8 1.9a2.3 2.3 0 0 1 3.2 2h2zm-6-4L4.4 15l.8 1.9 10.7-4.6-.7-1.8z"/></svg>',
'ordered-list-rtl': '<svg width="24" height="24"><path d="M6 17h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2Zm0-6h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2Zm0-6h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2Zm13-1v3.5a.5.5 0 1 1-1 0V5h-.5a.5.5 0 1 1 0-1H19Zm-1 8.8.2.2h1.3a.5.5 0 1 1 0 1h-1.6a1 1 0 0 1-.9-1V13c0-.4.3-.8.6-1l1.2-.4.2-.3a.2.2 0 0 0-.2-.2h-1.3a.5.5 0 0 1-.5-.5c0-.3.2-.5.5-.5h1.6c.5 0 .9.4.9 1v.1c0 .4-.3.8-.6 1l-1.2.4-.2.3Zm2 4.2v2c0 .6-.4 1-1 1h-1.5a.5.5 0 0 1 0-1h1.2a.3.3 0 1 0 0-.6h-1.3a.4.4 0 1 1 0-.8h1.3a.3.3 0 0 0 0-.6h-1.2a.5.5 0 1 1 0-1H19c.6 0 1 .4 1 1Z" fill-rule="evenodd"/></svg>',
'ordered-list': '<svg width="24" height="24"><path d="M10 17h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm0-6h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm0-6h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 1 1 0-2ZM6 4v3.5c0 .3-.2.5-.5.5a.5.5 0 0 1-.5-.5V5h-.5a.5.5 0 0 1 0-1H6Zm-1 8.8.2.2h1.3c.3 0 .5.2.5.5s-.2.5-.5.5H4.9a1 1 0 0 1-.9-1V13c0-.4.3-.8.6-1l1.2-.4.2-.3a.2.2 0 0 0-.2-.2H4.5a.5.5 0 0 1-.5-.5c0-.3.2-.5.5-.5h1.6c.5 0 .9.4.9 1v.1c0 .4-.3.8-.6 1l-1.2.4-.2.3ZM7 17v2c0 .6-.4 1-1 1H4.5a.5.5 0 0 1 0-1h1.2c.2 0 .3-.1.3-.3 0-.2-.1-.3-.3-.3H4.4a.4.4 0 1 1 0-.8h1.3c.2 0 .3-.1.3-.3 0-.2-.1-.3-.3-.3H4.5a.5.5 0 1 1 0-1H6c.6 0 1 .4 1 1Z" fill-rule="evenodd"/></svg>',
'orientation': '<svg width="24" height="24"><path d="M7.3 6.4 1 13l6.4 6.5 6.5-6.5-6.5-6.5ZM3.7 13l3.6-3.7L11 13l-3.7 3.7-3.6-3.7ZM12 6l2.8 2.7c.3.3.3.8 0 1-.3.4-.9.4-1.2 0L9.2 5.7a.8.8 0 0 1 0-1.2L13.6.2c.3-.3.9-.3 1.2 0 .3.3.3.8 0 1.1L12 4h1a9 9 0 1 1-4.3 16.9l1.5-1.5A7 7 0 1 0 13 6h-1Z" fill-rule="nonzero"/></svg>',
'other-actions': '<svg width="24" height="24"><path d="M8.5 13.5c.6 0 1 .2 1.4.6.4.4.6.8.6 1.4V19c0 .6-.2 1-.6 1.4a2 2 0 0 1-1.4.6H5a2 2 0 0 1-1.4-.6A2 2 0 0 1 3 19v-3.5c0-.6.2-1 .6-1.4a2 2 0 0 1 1.4-.6h3.5Zm10.5 0c.6 0 1 .2 1.4.6.4.4.6.8.6 1.4V19c0 .6-.2 1-.6 1.4a2 2 0 0 1-1.4.6h-3.5a2 2 0 0 1-1.4-.6 2 2 0 0 1-.6-1.4v-3.5c0-.6.2-1 .6-1.4a2 2 0 0 1 1.4-.6H19ZM5 19h3.5v-3.5H5V19Zm10.5 0H19v-3.5h-3.5V19Zm1-15.7a.5.5 0 0 1 1 0 5.5 5.5 0 0 0 3.2 3.3.5.5 0 0 1 0 .8l-.3.1a5.5 5.5 0 0 0-3 3.2.5.5 0 0 1-.8 0 5.5 5.5 0 0 0-3-3.2h-.3a.5.5 0 0 1 0-1 5.5 5.5 0 0 0 3.3-3.2Zm-8-.3c.6 0 1 .2 1.4.6.4.4.6.8.6 1.4v3.5c0 .6-.2 1-.6 1.4a2 2 0 0 1-1.4.6H5a2 2 0 0 1-1.4-.6A2 2 0 0 1 3 8.5V5c0-.6.2-1 .6-1.4A2 2 0 0 1 5 3h3.5ZM5 8.5h3.5V5H5v3.5Z"/></svg>',
'outdent': '<svg width="24" height="24"><path d="M7 5h12c.6 0 1 .4 1 1s-.4 1-1 1H7a1 1 0 1 1 0-2Zm5 4h7c.6 0 1 .4 1 1s-.4 1-1 1h-7a1 1 0 0 1 0-2Zm0 4h7c.6 0 1 .4 1 1s-.4 1-1 1h-7a1 1 0 0 1 0-2Zm-5 4h12a1 1 0 0 1 0 2H7a1 1 0 0 1 0-2Zm1.6-3.8a1 1 0 0 1-1.2 1.6l-3-2a1 1 0 0 1 0-1.6l3-2a1 1 0 0 1 1.2 1.6L6.8 12l1.8 1.2Z" fill-rule="evenodd"/></svg>',
'page-break': '<svg width="24" height="24"><g fill-rule="evenodd"><path d="M5 11c.6 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1 0-2Zm3 0h1c.6 0 1 .4 1 1s-.4 1-1 1H8a1 1 0 0 1 0-2Zm4 0c.6 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1 0-2Zm3 0h1c.6 0 1 .4 1 1s-.4 1-1 1h-1a1 1 0 0 1 0-2Zm4 0c.6 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1 0-2ZM7 3v5h10V3c0-.6.4-1 1-1s1 .4 1 1v7H5V3c0-.6.4-1 1-1s1 .4 1 1ZM6 22a1 1 0 0 1-1-1v-7h14v7c0 .6-.4 1-1 1a1 1 0 0 1-1-1v-5H7v5c0 .6-.4 1-1 1Z"/></g></svg>',
'paragraph': '<svg width="24" height="24"><path fill-rule="evenodd" d="M10 5h7a1 1 0 0 1 0 2h-1v11a1 1 0 0 1-2 0V7h-2v11a1 1 0 0 1-2 0v-6c-.5 0-1 0-1.4-.3A3.4 3.4 0 0 1 6.8 10a3.3 3.3 0 0 1 0-2.8 3.4 3.4 0 0 1 1.8-1.8L10 5Z"/></svg>',
'paste-column-after': '<svg width="24" height="24"><path fill-rule="evenodd" d="M12 1a3 3 0 0 1 2.8 2H18c1 0 2 .8 2 1.9V7h-2V5h-2v1c0 .6-.4 1-1 1H9a1 1 0 0 1-1-1V5H6v13h7v2H6c-1 0-2-.8-2-1.9V5c0-1 .8-2 1.9-2H9.2A3 3 0 0 1 12 1Zm8 7v12h-6V8h6Zm-1.5 1.5h-3v9h3v-9ZM12 3a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z"/></svg>',
'paste-column-before': '<svg width="24" height="24"><path fill-rule="evenodd" d="M12 1a3 3 0 0 1 2.8 2H18c1 0 2 .8 2 1.9V18c0 1-.8 2-1.9 2H11v-2h7V5h-2v1c0 .6-.4 1-1 1H9a1 1 0 0 1-1-1V5H6v2H4V5c0-1 .8-2 1.9-2H9.2A3 3 0 0 1 12 1Zm-2 7v12H4V8h6ZM8.5 9.5h-3v9h3v-9ZM12 3a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z"/></svg>',
'paste-row-after': '<svg width="24" height="24"><path fill-rule="evenodd" d="M12 1a3 3 0 0 1 2.8 2H18c1 0 2 .8 2 1.9V11h-2V5h-2v1c0 .6-.4 1-1 1H9a1 1 0 0 1-1-1V5H6v13h14c0 1-.8 2-1.9 2H6c-1 0-2-.8-2-1.9V5c0-1 .8-2 1.9-2H9.2A3 3 0 0 1 12 1Zm10 11v5H8v-5h14Zm-1.5 1.5h-11v2h11v-2ZM12 3a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z"/></svg>',
'paste-row-before': '<svg width="24" height="24"><path fill-rule="evenodd" d="M12 1a3 3 0 0 1 2.8 2H18c1 0 2 .8 2 1.9V7h-2V5h-2v1c0 .6-.4 1-1 1H9a1 1 0 0 1-1-1V5H6v13h12v-4h2v4c0 1-.8 2-1.9 2H6c-1 0-2-.8-2-1.9V5c0-1 .8-2 1.9-2H9.2A3 3 0 0 1 12 1Zm10 7v5H8V8h14Zm-1.5 1.5h-11v2h11v-2ZM12 3a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z"/></svg>',
'paste-text': '<svg width="24" height="24"><path d="M18 9V5h-2v1c0 .6-.4 1-1 1H9a1 1 0 0 1-1-1V5H6v13h3V9h9ZM9 20H6a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2h3.2A3 3 0 0 1 12 1a3 3 0 0 1 2.8 2H18a2 2 0 0 1 2 2v4h1v12H9v-1Zm1.5-9.5v9h9v-9h-9ZM12 3a1 1 0 0 0-1 1c0 .5.4 1 1 1s1-.5 1-1-.4-1-1-1Zm0 9h6v2h-.5l-.5-1h-1v4h.8v1h-3.6v-1h.8v-4h-1l-.5 1H12v-2Z" fill-rule="nonzero"/></svg>',
'paste': '<svg width="24" height="24"><path d="M18 9V5h-2v1c0 .6-.4 1-1 1H9a1 1 0 0 1-1-1V5H6v13h3V9h9ZM9 20H6a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2h3.2A3 3 0 0 1 12 1a3 3 0 0 1 2.8 2H18a2 2 0 0 1 2 2v4h1v12H9v-1Zm1.5-9.5v9h9v-9h-9ZM12 3a1 1 0 0 0-1 1c0 .5.4 1 1 1s1-.5 1-1-.4-1-1-1Z" fill-rule="nonzero"/></svg>',
'permanent-pen': '<svg width="24" height="24"><path d="M10.5 17.5 8 20H3v-3l3.5-3.5a2 2 0 0 1 0-3L14 3l1 1-7.3 7.3a1 1 0 0 0 0 1.4l3.6 3.6c.4.4 1 .4 1.4 0L20 9l1 1-7.6 7.6a2 2 0 0 1-2.8 0l-.1-.1Z" fill-rule="nonzero"/></svg>',
'photo-filter': '<svg height="24" width="24"><path d="M4.4 21c-.3 0-.6-.2-.9-.5-.3-.3-.4-.6-.5-.9L19.6 3c.3 0 .6.3.9.5l.5.9zM3 14.7v-2.8L11.9 3h2.8zM3 7V5c0-.6.2-1 .6-1.4A2 2 0 0 1 5 3h2zm14 14 4-4v2c0 .6-.2 1-.6 1.4a2 2 0 0 1-1.4.6zm-7.7 0L21 9.3v2.8L12 21z"/></svg>',
'pin': '<svg width="24" height="24"><path fill-rule="evenodd" d="M18 3a1 1 0 0 1 1 1v3.6a1 1 0 0 1-1 1h-1.6l2.5 6A1 1 0 0 1 18 16h-5v5h-2v-5H6a1 1 0 0 1-1-1.3l2.6-6H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h12ZM7 6.7h2A1 1 0 0 1 10 8l-2.6 6h9.2L14 8a1 1 0 0 1 .9-1.3h2V5H7v1.6Z"/></svg>',
'plus': '<svg width="24" height="24"><path d="M12 4c.5 0 1 .4 1 .9V11h6a1 1 0 0 1 .1 2H13v6a1 1 0 0 1-2 .1V13H5a1 1 0 0 1-.1-2H11V5c0-.6.4-1 1-1Z"/></svg>',
'preferences': '<svg width="24" height="24"><path d="m20.1 13.5-1.9.2a5.8 5.8 0 0 1-.6 1.5l1.2 1.5c.4.4.3 1 0 1.4l-.7.7a1 1 0 0 1-1.4 0l-1.5-1.2a6.2 6.2 0 0 1-1.5.6l-.2 1.9c0 .5-.5.9-1 .9h-1a1 1 0 0 1-1-.9l-.2-1.9a5.8 5.8 0 0 1-1.5-.6l-1.5 1.2a1 1 0 0 1-1.4 0l-.7-.7a1 1 0 0 1 0-1.4l1.2-1.5a6.2 6.2 0 0 1-.6-1.5l-1.9-.2a1 1 0 0 1-.9-1v-1c0-.5.4-1 .9-1l1.9-.2a5.8 5.8 0 0 1 .6-1.5L5.2 7.3a1 1 0 0 1 0-1.4l.7-.7a1 1 0 0 1 1.4 0l1.5 1.2a6.2 6.2 0 0 1 1.5-.6l.2-1.9c0-.5.5-.9 1-.9h1c.5 0 1 .4 1 .9l.2 1.9a5.8 5.8 0 0 1 1.5.6l1.5-1.2a1 1 0 0 1 1.4 0l.7.7c.3.4.4 1 0 1.4l-1.2 1.5a6.2 6.2 0 0 1 .6 1.5l1.9.2c.5 0 .9.5.9 1v1c0 .5-.4 1-.9 1ZM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" fill-rule="evenodd"/></svg>',
'preview': '<svg width="24" height="24"><path d="M3.5 12.5c.5.8 1.1 1.6 1.8 2.3 2 2 4.2 3.2 6.7 3.2s4.7-1.2 6.7-3.2a16.2 16.2 0 0 0 2.1-2.8 15.7 15.7 0 0 0-2.1-2.8c-2-2-4.2-3.2-6.7-3.2a9.3 9.3 0 0 0-6.7 3.2A16.2 16.2 0 0 0 3.2 12c0 .2.2.3.3.5Zm-2.4-1 .7-1.2L4 7.8C6.2 5.4 8.9 4 12 4c3 0 5.8 1.4 8.1 3.8a18.2 18.2 0 0 1 2.8 3.7v1l-.7 1.2-2.1 2.5c-2.3 2.4-5 3.8-8.1 3.8-3 0-5.8-1.4-8.1-3.8a18.2 18.2 0 0 1-2.8-3.7 1 1 0 0 1 0-1Zm12-3.3a2 2 0 1 0 2.7 2.6 4 4 0 1 1-2.6-2.6Z" fill-rule="nonzero"/></svg>',
'print': '<svg width="24" height="24"><path d="M18 8H6a3 3 0 0 0-3 3v6h2v3h14v-3h2v-6a3 3 0 0 0-3-3Zm-1 10H7v-4h10v4Zm.5-5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5 1.5.7 1.5 1.5-.7 1.5-1.5 1.5Zm.5-8H6v2h12V5Z" fill-rule="nonzero"/></svg>',
'quote': '<svg width="24" height="24"><path d="M7.5 17h.9c.4 0 .7-.2.9-.6L11 13V8c0-.6-.4-1-1-1H6a1 1 0 0 0-1 1v4c0 .6.4 1 1 1h2l-1.3 2.7a1 1 0 0 0 .8 1.3Zm8 0h.9c.4 0 .7-.2.9-.6L19 13V8c0-.6-.4-1-1-1h-4a1 1 0 0 0-1 1v4c0 .6.4 1 1 1h2l-1.3 2.7a1 1 0 0 0 .8 1.3Z" fill-rule="nonzero"/></svg>',
'reasoning': '<svg height="24" width="24"><path d="M14.3 22h-3.6v-2h3.6zM15.3 18.4H9.7v-2h5.6zM12.8 2c1.6 0 3.1.7 4.3 1.9C18.3 5 19 6.7 19 8.4c0 1.4-.5 2.8-1.7 3.9l-.2.2a3 3 0 0 0-.9 1.5 1 1 0 1 1-2-.4c.3-1.2 1-2 1.7-2.8.7-.6 1.1-1.5 1.1-2.4 0-1.2-.5-2.3-1.3-3.1a4.5 4.5 0 0 0-3-1.3h-.2c-1.2 0-2.3.5-3.2 1.3C8.5 6 8 7.3 8 8.4c0 .7.1 1.5 1 2.4.8.7 1.5 1.6 1.7 2.8a1 1 0 0 1-2 .4c0-.6-.4-1.2-1-1.7A5 5 0 0 1 6 8.4c0-1.7.7-3.3 2-4.5A6.6 6.6 0 0 1 12.4 2z"/></svg>',
'redo': '<svg width="24" height="24"><path d="M17.6 10H12c-2.8 0-4.4 1.4-4.9 3.5-.4 2 .3 4 1.4 4.6a1 1 0 1 1-1 1.8c-2-1.2-2.9-4.1-2.3-6.8.6-3 3-5.1 6.8-5.1h5.6l-3.3-3.3a1 1 0 1 1 1.4-1.4l5 5a1 1 0 0 1 0 1.4l-5 5a1 1 0 0 1-1.4-1.4l3.3-3.3Z" fill-rule="nonzero"/></svg>',
'reload': '<svg width="24" height="24"><g fill-rule="nonzero"><path d="m5 22.1-1.2-4.7v-.2a1 1 0 0 1 1-1l5 .4a1 1 0 1 1-.2 2l-2.2-.2a7.8 7.8 0 0 0 8.4.2 7.5 7.5 0 0 0 3.5-6.4 1 1 0 1 1 2 0 9.5 9.5 0 0 1-4.5 8 9.9 9.9 0 0 1-10.2 0l.4 1.4a1 1 0 1 1-2 .5ZM13.6 7.4c0-.5.5-1 1-.9l2.8.2a8 8 0 0 0-9.5-1 7.5 7.5 0 0 0-3.6 7 1 1 0 0 1-2 0 9.5 9.5 0 0 1 4.5-8.6 10 10 0 0 1 10.9.3l-.3-1a1 1 0 0 1 2-.5l1.1 4.8a1 1 0 0 1-1 1.2l-5-.4a1 1 0 0 1-.9-1Z"/></g></svg>',
'remove-formatting': '<svg width="24" height="24"><path d="M13.2 6a1 1 0 0 1 0 .2l-2.6 10a1 1 0 0 1-1 .8h-.2a.8.8 0 0 1-.8-1l2.6-10H8a1 1 0 1 1 0-2h9a1 1 0 0 1 0 2h-3.8ZM5 18h7a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Zm13 1.5L16.5 18 15 19.5a.7.7 0 0 1-1-1l1.5-1.5-1.5-1.5a.7.7 0 0 1 1-1l1.5 1.5 1.5-1.5a.7.7 0 0 1 1 1L17.5 17l1.5 1.5a.7.7 0 0 1-1 1Z" fill-rule="evenodd"/></svg>',
'remove': '<svg width="24" height="24"><path d="M16 7h3a1 1 0 0 1 0 2h-1v9a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V9H5a1 1 0 1 1 0-2h3V6a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v1Zm-2 0V6c0-.6-.4-1-1-1h-2a1 1 0 0 0-1 1v1h4Zm2 2H8v9c0 .6.4 1 1 1h6c.6 0 1-.4 1-1V9Zm-7 3a1 1 0 0 1 2 0v4a1 1 0 0 1-2 0v-4Zm4 0a1 1 0 0 1 2 0v4a1 1 0 0 1-2 0v-4Z" fill-rule="nonzero"/></svg>',
'resize-handle': '<svg width="10" height="10"><g fill-rule="nonzero"><path d="M8.1 1.1A.5.5 0 1 1 9 2l-7 7A.5.5 0 1 1 1 8l7-7ZM8.1 5.1A.5.5 0 1 1 9 6l-3 3A.5.5 0 1 1 5 8l3-3Z"/></g></svg>',
'resize': '<svg width="24" height="24"><path d="M4 5c0-.3.1-.5.3-.7.2-.2.4-.3.7-.3h6c.3 0 .5.1.7.3.2.2.3.4.3.7 0 .3-.1.5-.3.7a1 1 0 0 1-.7.3H7.4L18 16.6V13c0-.3.1-.5.3-.7.2-.2.4-.3.7-.3.3 0 .5.1.7.3.2.2.3.4.3.7v6c0 .3-.1.5-.3.7a1 1 0 0 1-.7.3h-6a1 1 0 0 1-.7-.3 1 1 0 0 1-.3-.7c0-.3.1-.5.3-.7.2-.2.4-.3.7-.3h3.6L6 7.4V11c0 .3-.1.5-.3.7a1 1 0 0 1-.7.3 1 1 0 0 1-.7-.3A1 1 0 0 1 4 11V5Z" fill-rule="evenodd"/></svg>',
'restore-draft': '<svg width="24" height="24"><g fill-rule="evenodd"><path d="M17 13c0 .6-.4 1-1 1h-4V8c0-.6.4-1 1-1s1 .4 1 1v4h2c.6 0 1 .4 1 1Z"/><path d="M4.7 10H9a1 1 0 0 1 0 2H3a1 1 0 0 1-1-1V5a1 1 0 1 1 2 0v3l2.5-2.4a9.2 9.2 0 0 1 10.8-1.5A9 9 0 0 1 13.4 21c-2.4.1-4.7-.7-6.5-2.2a1 1 0 1 1 1.3-1.5 7.2 7.2 0 0 0 11.6-3.7 7 7 0 0 0-3.5-7.7A7.2 7.2 0 0 0 8 7L4.7 10Z" fill-rule="nonzero"/></g></svg>',
'revert-changes': '<svg height="24" width="24"><path d="m8.9 18.8.4 3.2H13v-2h-2v-2.6a4.6 4.6 0 0 1-1.6-.6 5.9 5.9 0 0 1-1.3-1l-2.5 1-1-1.7 2.2-1.6a4 4 0 0 1-.2-.7 5.9 5.9 0 0 1 0-.8v-.8l.2-.8-2.2-1.6 1-1.7 2.5 1a6 6 0 0 1 1.2-.9l1.4-.6.4-2.6h2l.3 2.6A5.6 5.6 0 0 1 16 8.2l2.5-1 1 1.6-2.2 1.7.2.7v.8a5 5 0 0 1 0 1h2a2.6 2.6 0 0 0 0-.5V11.3l2.6-2-2.8-4.7-3 1.3a8.2 8.2 0 0 0-.5-.4 3.8 3.8 0 0 0-.6-.3L14.8 2H9.3l-.4 3.2a5 5 0 0 0-1.2.7l-3-1.3L2 9.4l2.6 2V12.6l-2.6 2 2.7 4.7 3-1.3a8 8 0 0 0 1.2.7z"/><path d="m14.5 20.3 2.1-2-2.1-2.2 1.4-1.4 2.1 2.1 2.1-2 1.4 1.3-2 2.1 2 2.1-1.4 1.4-2.1-2-2.1 2zM12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></svg>',
'revision-history': '<svg width="24" height="24"><g fill-rule="evenodd"><path d="M17 13c0 .6-.4 1-1 1h-4V8c0-.6.4-1 1-1s1 .4 1 1v4h2c.6 0 1 .4 1 1Z"/><path d="M4.7 10H9a1 1 0 0 1 0 2H3a1 1 0 0 1-1-1V5a1 1 0 1 1 2 0v3l2.5-2.4a9.2 9.2 0 0 1 10.8-1.5A9 9 0 0 1 13.4 21c-2.4.1-4.7-.7-6.5-2.2a1 1 0 1 1 1.3-1.5 7.2 7.2 0 0 0 11.6-3.7 7 7 0 0 0-3.5-7.7A7.2 7.2 0 0 0 8 7L4.7 10Z" fill-rule="nonzero"/></g></svg>',
'rotate-left': '<svg width="24" height="24"><path d="M4.7 10H9a1 1 0 0 1 0 2H3a1 1 0 0 1-1-1V5a1 1 0 1 1 2 0v3l2.5-2.4a9.2 9.2 0 0 1 10.8-1.5A9 9 0 0 1 13.4 21c-2.4.1-4.7-.7-6.5-2.2a1 1 0 1 1 1.3-1.5 7.2 7.2 0 0 0 11.6-3.7 7 7 0 0 0-3.5-7.7A7.2 7.2 0 0 0 8 7L4.7 10Z" fill-rule="nonzero"/></svg>',
'rotate-right': '<svg width="24" height="24"><path d="M20 8V5a1 1 0 0 1 2 0v6c0 .6-.4 1-1 1h-6a1 1 0 0 1 0-2h4.3L16 7A7.2 7.2 0 0 0 7.7 6a7 7 0 0 0 3 13.1c1.9.1 3.7-.5 5-1.7a1 1 0 0 1 1.4 1.5A9.2 9.2 0 0 1 2.2 14c-.9-3.9 1-8 4.5-9.9 3.5-1.9 8-1.3 10.8 1.5L20 8Z" fill-rule="nonzero"/></svg>',
'rtl': '<svg width="24" height="24"><path d="M8 5h8v2h-2v12h-2V7h-2v12H8v-7c-.5 0-1 0-1.4-.3A3.4 3.4 0 0 1 4.8 10a3.3 3.3 0 0 1 0-2.8 3.4 3.4 0 0 1 1.8-1.8L8 5Zm12 11.2a1 1 0 1 1-1 1.6l-3-2a1 1 0 0 1 0-1.6l3-2a1 1 0 1 1 1 1.6L18.4 15l1.8 1.2Z" fill-rule="evenodd"/></svg>',
'saturation': '<svg width="24" height="24"><path fill-rule="evenodd" d="M12 20.5a6 6 0 1 1-5.9-10.2 6 6 0 1 1 11.8 0A6 6 0 1 1 12 20.5Zm-1.1-7.7a6 6 0 0 0-.2.3 4.3 4.3 0 0 1-2-1.3c.9.1 1.6.5 2.2 1Zm2.4.3a6 6 0 0 0-.2-.3 4.3 4.3 0 0 1 2.2-1 4.3 4.3 0 0 1-2 1.3ZM12.2 15a6.3 6.3 0 0 1-.4 0 4.3 4.3 0 0 0 .2 2.5 4.3 4.3 0 0 0 .2-2.5Zm.9 4.2a6 6 0 0 0 .8-4.5 6 6 0 0 0 3.4-2.8 4.3 4.3 0 0 1-1.3 8.4 4.3 4.3 0 0 1-2.9-1.1Zm-2.2 0a4.3 4.3 0 1 1-4.2-7.3 6 6 0 0 0 3.4 2.8 6 6 0 0 0 .8 4.5Zm-3-9.2a4.3 4.3 0 1 1 8.3 0 6 6 0 0 0-4.2 1.5A6 6 0 0 0 7.8 10Z"/></svg>',
'save': '<svg width="24" height="24"><path d="M5 16h14a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2c0-1.1.9-2 2-2Zm0 2v2h14v-2H5Zm10 0h2v2h-2v-2Zm-4-6.4L8.7 9.3a1 1 0 1 0-1.4 1.4l4 4c.4.4 1 .4 1.4 0l4-4a1 1 0 1 0-1.4-1.4L13 11.6V4a1 1 0 0 0-2 0v7.6Z" fill-rule="nonzero"/></svg>',
'search': '<svg width="24" height="24"><path d="M16 17.3a8 8 0 1 1 1.4-1.4l4.3 4.4a1 1 0 0 1-1.4 1.4l-4.4-4.3Zm-5-.3a6 6 0 1 0 0-12 6 6 0 0 0 0 12Z" fill-rule="nonzero"/></svg>',
'select-all': '<svg width="24" height="24"><path d="M3 5h2V3a2 2 0 0 0-2 2Zm0 8h2v-2H3v2Zm4 8h2v-2H7v2ZM3 9h2V7H3v2Zm10-6h-2v2h2V3Zm6 0v2h2a2 2 0 0 0-2-2ZM5 21v-2H3c0 1.1.9 2 2 2Zm-2-4h2v-2H3v2ZM9 3H7v2h2V3Zm2 18h2v-2h-2v2Zm8-8h2v-2h-2v2Zm0 8a2 2 0 0 0 2-2h-2v2Zm0-12h2V7h-2v2Zm0 8h2v-2h-2v2Zm-4 4h2v-2h-2v2Zm0-16h2V3h-2v2ZM7 17h10V7H7v10Zm2-8h6v6H9V9Z" fill-rule="nonzero"/></svg>',
'selected': '<svg width="24" height="24"><path fill-rule="nonzero" d="M6 4h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2Zm3.6 10.9L7 12.3a.7.7 0 0 0-1 1L9.6 17 18 8.6a.7.7 0 0 0 0-1 .7.7 0 0 0-1 0l-7.4 7.3Z"/></svg>',
'send': '<svg width="24" height="24"><path fill-rule="evenodd" clip-rule="evenodd" d="m13.3 22 7-18.3-18.3 7L9 15l4.3 7ZM18 6.8l-.7-.7L9.4 14l.7.7L18 6.8Z"/></svg>',
'settings': '<svg width="24" height="24"><path d="M11 6h8c.6 0 1 .4 1 1s-.4 1-1 1h-8v.3c0 .2 0 .3-.2.5l-.6.2H7.8c-.3 0-.4 0-.6-.2a.7.7 0 0 1-.2-.6V8H5a1 1 0 1 1 0-2h2v-.3c0-.2 0-.3.2-.5l.5-.2h2.5c.3 0 .4 0 .6.2l.2.5V6ZM8 8h2V6H8v2Zm9 2.8v.2h2c.6 0 1 .4 1 1s-.4 1-1 1h-2v.3c0 .2 0 .3-.2.5l-.6.2h-2.4c-.3 0-.4 0-.6-.2a.7.7 0 0 1-.2-.6V13H5a1 1 0 0 1 0-2h8v-.3c0-.2 0-.3.2-.5l.6-.2h2.4c.3 0 .4 0 .6.2l.2.6ZM14 13h2v-2h-2v2Zm-3 2.8v.2h8c.6 0 1 .4 1 1s-.4 1-1 1h-8v.3c0 .2 0 .3-.2.5l-.6.2H7.8c-.3 0-.4 0-.6-.2a.7.7 0 0 1-.2-.6V18H5a1 1 0 0 1 0-2h2v-.3c0-.2 0-.3.2-.5l.5-.2h2.5c.3 0 .4 0 .6.2l.2.6ZM8 18h2v-2H8v2Z" fill-rule="evenodd"/></svg>',
'sharpen': '<svg width="24" height="24"><path fill-rule="evenodd" d="m16 6 4 4-8 9-8-9 4-4h8Zm-4 10.2 5.5-6.2-.1-.1H12v-.3h5.1l-.2-.2H12V9h4.6l-.2-.2H12v-.3h4.1l-.2-.2H12V8h3.6l-.2-.2H8.7L6.5 10l.1.1H12v.3H6.9l.2.2H12v.3H7.3l.2.2H12v.3H7.7l.3.2h4v.3H8.2l.2.2H12v.3H8.6l.3.2H12v.3H9l.3.2H12v.3H9.5l.2.2H12v.3h-2l.2.2H12v.3h-1.6l.2.2H12v.3h-1.1l.2.2h.9v.3h-.7l.2.2h.5v.3h-.3l.3.2Z"/></svg>',
'source-close': '<svg width="16" height="16"><path fill-rule="evenodd" d="M11.8 5 9 8l3 3a.6.6 0 0 1-1 .8L8 9l-3 3a.6.6 0 0 1-.8-1L7 8l-3-3a.6.6 0 0 1 1-.8L8 7l3-3a.6.6 0 0 1 .8 1Z"/></svg>',
'source-file': '<svg width="16" height="16"><path fill-rule="evenodd" d="M4.3 2H10L13 5v7.7c0 .7-.6 1.3-1.3 1.3H4.3A1.3 1.3 0 0 1 3 12.7V3.3C3 2.6 3.6 2 4.3 2Zm0 10.7h7.4V6H9V3.3H4.3v9.4Z"/></svg>',
'source-image': '<svg height="16" width="16"><path d="M2.7 2h10.6c.4 0 .7.3.7.7v10.6a.7.7 0 0 1-.7.7H2.7a.7.7 0 0 1-.7-.7V2.7c0-.4.3-.7.7-.7zm.6 8.5 2.2-2.2a.5.5 0 0 1 .7 0L8 10l2.8-2.7a.5.5 0 0 1 .7 0l1.3 1.3V3.3H3.3v7.2zm0 1.4v.8h2l2-2-1.4-1.3zM11 8.4l1.7 1.6v2.7h-6L11 8.4z" fill-rule="evenodd"/><path d="M8.7 6.3a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/></svg>',
'source-link': '<svg height="16" width="16"><g fill-rule="evenodd"><path d="M3.5 12.6a2.7 2.7 0 0 1 0-3.8l.5-.6a.7.7 0 1 1 1 1l-.6.5a1.3 1.3 0 1 0 1.9 2L7.9 10a2.1 2.1 0 0 0 .6-2 .7.7 0 0 1 1.3-.3 3.4 3.4 0 0 1-1 3.2l-1.6 1.6a2.7 2.7 0 0 1-3.8 0z"/><path d="M12.6 3.5a2.7 2.7 0 0 1 0 3.7l-.6.6a.7.7 0 1 1-1-1l.6-.5a1.3 1.3 0 1 0-1.9-2L8.1 6a2.1 2.1 0 0 0-.6 2 .7.7 0 1 1-1.3.3 3.4 3.4 0 0 1 1-3.2l1.6-1.7a2.7 2.7 0 0 1 3.8 0z"/></g></svg>',
'source-selection': '<svg width="16" height="16"><path d="M3.2 12.8h.7V14H2.6a.6.6 0 0 1-.6-.6v-1.3h1.2v.7ZM7.5 14H4.9v-1.2h2.6V14Zm5.9-4.8a.6.6 0 1 1 0 1.2h-2.2l2.6 2.6a.6.6 0 1 1-.8.8l-2.6-2.6v2.2a.6.6 0 1 1-1.2 0V9.2h4.2ZM3.2 11.1H2V8.5h1.2v2.6Zm0-3.6H2V4.9h1.2v2.6Zm10.8 0h-1.2V4.9H14v2.6ZM3.9 3.2h-.7v.7H2V2.6a.6.6 0 0 1 .6-.6h1.3v1.2ZM13.4 2a.6.6 0 0 1 .6.6v1.3h-1.2v-.7h-.7V2h1.3Zm-6 1.2H5V2h2.6v1.2Zm3.7 0H8.5V2h2.6v1.2Z"/></svg>',
'sourcecode': '<svg width="24" height="24"><g fill-rule="nonzero"><path d="M9.8 15.7c.3.3.3.8 0 1-.3.4-.9.4-1.2 0l-4.4-4.1a.8.8 0 0 1 0-1.2l4.4-4.2c.3-.3.9-.3 1.2 0 .3.3.3.8 0 1.1L6 12l3.8 3.7ZM14.2 15.7c-.3.3-.3.8 0 1 .4.4.9.4 1.2 0l4.4-4.1c.3-.3.3-.9 0-1.2l-4.4-4.2a.8.8 0 0 0-1.2 0c-.3.3-.3.8 0 1.1L18 12l-3.8 3.7Z"/></g></svg>',
'spell-check': '<svg width="24" height="24"><path d="M6 8v3H5V5c0-.3.1-.5.3-.7.2-.2.4-.3.7-.3h2c.3 0 .5.1.7.3.2.2.3.4.3.7v6H8V8H6Zm0-3v2h2V5H6Zm13 0h-3v5h3v1h-3a1 1 0 0 1-.7-.3 1 1 0 0 1-.3-.7V5c0-.3.1-.5.3-.7.2-.2.4-.3.7-.3h3v1Zm-5 1.5-.1.7c-.1.2-.3.3-.6.3.3 0 .5.1.6.3l.1.7V10c0 .3-.1.5-.3.7a1 1 0 0 1-.7.3h-3V4h3c.3 0 .5.1.7.3.2.2.3.4.3.7v1.5ZM13 10V8h-2v2h2Zm0-3V5h-2v2h2Zm3 5 1 1-6.5 7L7 15.5l1.3-1 2.2 2.2L16 12Z" fill-rule="evenodd"/></svg>',
'stop': '<svg height="24" width="24"><path d="M19 17a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>',
'strike-through': '<svg width="24" height="24"><g fill-rule="evenodd"><path d="M15.6 8.5c-.5-.7-1-1.1-1.3-1.3-.6-.4-1.3-.6-2-.6-2.7 0-2.8 1.7-2.8 2.1 0 1.6 1.8 2 3.2 2.3 4.4.9 4.6 2.8 4.6 3.9 0 1.4-.7 4.1-5 4.1A6.2 6.2 0 0 1 7 16.4l1.5-1.1c.4.6 1.6 2 3.7 2 1.6 0 2.5-.4 3-1.2.4-.8.3-2-.8-2.6-.7-.4-1.6-.7-2.9-1-1-.2-3.9-.8-3.9-3.6C7.6 6 10.3 5 12.4 5c2.9 0 4.2 1.6 4.7 2.4l-1.5 1.1Z"/><path d="M5 11h14a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z" fill-rule="nonzero"/></g></svg>',
'subscript': '<svg width="24" height="24"><path d="m10.4 10 4.6 4.6-1.4 1.4L9 11.4 4.4 16 3 14.6 7.6 10 3 5.4 4.4 4 9 8.6 13.6 4 15 5.4 10.4 10ZM21 19h-5v-1l1-.8 1.7-1.6c.3-.4.5-.8.5-1.2 0-.3 0-.6-.2-.7-.2-.2-.5-.3-.9-.3a2 2 0 0 0-.8.2l-.7.3-.4-1.1 1-.6 1.2-.2c.8 0 1.4.3 1.8.7.4.4.6.9.6 1.5s-.2 1.1-.5 1.6a8 8 0 0 1-1.3 1.3l-.6.6h2.6V19Z" fill-rule="nonzero"/></svg>',
'suggestededits-badge': '<svg height="24" width="24"><path d="M11 3H7a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h10a2 2 0 0 0 2-2V9.7a6 6 0 0 1-2 .3v9H7V5h4a6 6 0 0 1 0-2z"/><path d="M15 13v-2h-4v2zm-3 2H9v2h3zm-1-8H9v2h2zm4 10v-2h-2v2z"/><path d="M21 4a4 4 0 1 1-8 0 4 4 0 0 1 8 0z" class="tox-icon--badge"/></svg>',
'suggestededits': '<svg width="24" height="24"><path d="M7 3h7.4L19 7.6V19a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2Zm0 16h10V9h-3V5H7v14Z" clip-rule="evenodd"/><path d="M9 7h2v2H9V7Zm2 4h4v2h-4v-2Zm-2 4h3v2H9v-2Z"/><path d="M9 7h2v2H9V7Zm4 8h2v2h-2v-2Z"/></svg>',
'summarize': '<svg width="24" height="24"><path fill-rule="evenodd" d="M14 3c.3 0 .6 0 .8.2-1.3 1-1 3.3.7 4h.2l.3.2A2 2 0 0 1 14 9h-4a2 2 0 0 1-2-2H6v13h12V9.8a2.4 2.4 0 0 0 2 0V21a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h3a2 2 0 0 1 2-2h4Zm-4 4h4V5h-4v2Z"/><path d="M15 16a1 1 0 1 1 0 2H9a1 1 0 1 1 0-2h6Zm0-4a1 1 0 1 1 0 2H9a1 1 0 1 1 0-2h6Zm3.7-9.8a.3.3 0 0 1 .6 0 4.1 4.1 0 0 0 2.5 2.5.3.3 0 0 1 0 .6l-.2.1a4.1 4.1 0 0 0-2.1 2v.1l-.2.3V8a.3.3 0 0 1-.1 0H19a.3.3 0 0 1 0 .1.4.4 0 0 1-.2 0 .3.3 0 0 1-.1 0v-.1a.3.3 0 0 1-.1 0v-.1a4.1 4.1 0 0 0-2.3-2.4h-.2a.3.3 0 0 1 0-.7 4.1 4.1 0 0 0 2.5-2.5Z"/></svg>',
'superscript': '<svg width="24" height="24"><path d="M15 9.4 10.4 14l4.6 4.6-1.4 1.4L9 15.4 4.4 20 3 18.6 7.6 14 3 9.4 4.4 8 9 12.6 13.6 8 15 9.4Zm5.9 1.6h-5v-1l1-.8 1.7-1.6c.3-.5.5-.9.5-1.3 0-.3 0-.5-.2-.7-.2-.2-.5-.3-.9-.3l-.8.2-.7.4-.4-1.2c.2-.2.5-.4 1-.5.3-.2.8-.2 1.2-.2.8 0 1.4.2 1.8.6.4.4.6 1 .6 1.6 0 .5-.2 1-.5 1.5l-1.3 1.4-.6.5h2.6V11Z" fill-rule="nonzero"/></svg>',
'table-caption': '<svg width="24" height="24"><g fill-rule="nonzero"><rect width="12" height="2" x="3" y="4" rx="1"/><path d="M19 8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-8c0-1.1.9-2 2-2h14ZM5 15v3h6v-3H5Zm14 0h-6v3h6v-3Zm0-5h-6v3h6v-3ZM5 13h6v-3H5v3Z"/></g></svg>',
'table-cell-classes': '<svg width="24" height="24"><g fill-rule="evenodd"><path fill-rule="nonzero" d="M13 4v9H3V6c0-1.1.9-2 2-2h8Zm-2 2H5v5h6V6Z"/><path fill-rule="nonzero" d="M13 4h6a2 2 0 0 1 2 2v7h-8v-2h6V6h-6V4Z" opacity=".2"/><path d="m18 20-2.6 1.6.7-3-2.4-2 3.1-.2 1.2-2.9 1.2 2.9 3.1.2-2.4 2 .7 3z"/><path fill-rule="nonzero" d="M3 13v5c0 1.1.9 2 2 2h8v-7h-2v5H5v-5H3Z" opacity=".2"/></g></svg>',
'table-cell-properties': '<svg width="24" height="24"><path fill-rule="nonzero" d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h14Zm-8 9H5v5h6v-5Zm8 0h-6v5h6v-5Zm-8-7H5v5h6V6Z"/></svg>',
'table-cell-select-all': '<svg width="24" height="24"><g fill-rule="evenodd"><path fill-rule="nonzero" d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h14Zm0 2H5v12h14V6Z"/><path d="M13 6v5h6v2h-6v5h-2v-5H5v-2h6V6h2Z" opacity=".2"/></g></svg>',
'table-cell-select-inner': '<svg width="24" height="24"><g fill-rule="evenodd"><path fill-rule="nonzero" d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h14Zm0 2H5v12h14V6Z" opacity=".2"/><path d="M13 6v5h6v2h-6v5h-2v-5H5v-2h6V6h2Z"/></g></svg>',
'table-classes': '<svg width="24" height="24"><g fill-rule="evenodd"><path fill-rule="nonzero" d="M19 4a2 2 0 0 1 2 2v7h-8v7H5a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h14Zm-8 9H5v5h6v-5Zm8-7h-6v5h6V6Zm-8 0H5v5h6V6Z"/><path d="m18 20-2.6 1.6.7-3-2.4-2 3.1-.2 1.2-2.9 1.2 2.9 3.1.2-2.4 2 .7 3z"/></g></svg>',
'table-delete-column': '<svg width="24" height="24"><path fill-rule="nonzero" d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h14Zm-4 4h-2V6h-2v2H9V6H5v12h4v-2h2v2h2v-2h2v2h4V6h-4v2Zm.3.5 1 1.2-3 2.3 3 2.3-1 1.2L12 13l-3.3 2.6-1-1.2 3-2.3-3-2.3 1-1.2L12 11l3.3-2.5Z"/></svg>',
'table-delete-row': '<svg width="24" height="24"><path fill-rule="nonzero" d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h14Zm0 2H5v3h2.5v2H5v2h2.5v2H5v3h14v-3h-2.5v-2H19v-2h-2.5V9H19V6Zm-4.7 1.8 1.2 1L13 12l2.6 3.3-1.2 1-2.3-3-2.3 3-1.2-1L11 12 8.5 8.7l1.2-1 2.3 3 2.3-3Z"/></svg>',
'table-delete-table': '<svg width="24" height="24"><g fill-rule="nonzero"><path d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h14ZM5 6v12h14V6H5Z"/><path d="m14.4 8.6 1.1 1-2.4 2.4 2.4 2.4-1.1 1.1-2.4-2.4-2.4 2.4-1-1.1 2.3-2.4-2.3-2.4 1-1 2.4 2.3z"/></g></svg>',
'table-insert-column-after': '<svg width="24" height="24"><path fill-rule="nonzero" d="M20 4c.6 0 1 .4 1 1v2a1 1 0 0 1-2 0V6h-8v12h8v-1a1 1 0 0 1 2 0v2c0 .5-.4 1-.9 1H5a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h15ZM9 13H5v5h4v-5Zm7-5c.5 0 1 .4 1 .9V11h2a1 1 0 0 1 .1 2H17v2a1 1 0 0 1-2 .1V13h-2a1 1 0 0 1-.1-2H15V9c0-.6.4-1 1-1ZM9 6H5v5h4V6Z"/></svg>',
'table-insert-column-before': '<svg width="24" height="24"><path fill-rule="nonzero" d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a1 1 0 0 1-1-1v-2a1 1 0 0 1 2 0v1h8V6H5v1a1 1 0 1 1-2 0V5c0-.6.4-1 1-1h15Zm0 9h-4v5h4v-5ZM8 8c.5 0 1 .4 1 .9V11h2a1 1 0 0 1 .1 2H9v2a1 1 0 0 1-2 .1V13H5a1 1 0 0 1-.1-2H7V9c0-.6.4-1 1-1Zm11-2h-4v5h4V6Z"/></svg>',
'table-insert-row-above': '<svg width="24" height="24"><path fill-rule="nonzero" d="M6 4a1 1 0 1 1 0 2H5v6h14V6h-1a1 1 0 0 1 0-2h2c.6 0 1 .4 1 1v13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5c0-.6.4-1 1-1h2Zm5 10H5v4h6v-4Zm8 0h-6v4h6v-4ZM12 3c.5 0 1 .4 1 .9V6h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 .1V8H9a1 1 0 0 1 0-2h2V4c0-.6.4-1 1-1Z"/></svg>',
'table-insert-row-after': '<svg width="24" height="24"><path fill-rule="nonzero" d="M12 13c.5 0 1 .4 1 .9V16h2a1 1 0 0 1 .1 2H13v2a1 1 0 0 1-2 .1V18H9a1 1 0 0 1-.1-2H11v-2c0-.6.4-1 1-1Zm6 7a1 1 0 0 1 0-2h1v-6H5v6h1a1 1 0 0 1 0 2H4a1 1 0 0 1-1-1V6c0-1.1.9-2 2-2h14a2 2 0 0 1 2 2v13c0 .5-.4 1-.9 1H18ZM11 6H5v4h6V6Zm8 0h-6v4h6V6Z"/></svg>',
'table-left-header': '<svg width="24" height="24"><path d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h14Zm0 9h-4v5h4v-5Zm-6 0H9v5h4v-5Zm0-7H9v5h4V6Zm6 0h-4v5h4V6Z"/></svg>',
'table-merge-cells': '<svg width="24" height="24"><path fill-rule="nonzero" d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h14ZM5 15.5V18h3v-2.5H5Zm14-5h-9V18h9v-7.5ZM19 6h-4v2.5h4V6ZM8 6H5v2.5h3V6Zm5 0h-3v2.5h3V6Zm-8 7.5h3v-3H5v3Z"/></svg>',
'table-row-numbering-rtl': '<svg width="24" height="24"><path d="M6 4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6Zm0 12h8v3H6v-3Zm11 0c.6 0 1 .4 1 1v1a1 1 0 0 1-2 0v-1c0-.6.4-1 1-1ZM6 11h8v3H6v-3Zm11 0c.6 0 1 .4 1 1v1a1 1 0 0 1-2 0v-1c0-.6.4-1 1-1ZM6 6h8v3H6V6Zm11 0c.6 0 1 .4 1 1v1a1 1 0 1 1-2 0V7c0-.6.4-1 1-1Z"/></svg>',
'table-row-numbering': '<svg width="24" height="24"><path d="M18 4a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h12Zm0 12h-8v3h8v-3ZM7 16a1 1 0 0 0-1 1v1a1 1 0 0 0 2 0v-1c0-.6-.4-1-1-1Zm11-5h-8v3h8v-3ZM7 11a1 1 0 0 0-1 1v1a1 1 0 0 0 2 0v-1c0-.6-.4-1-1-1Zm11-5h-8v3h8V6ZM7 6a1 1 0 0 0-1 1v1a1 1 0 1 0 2 0V7c0-.6-.4-1-1-1Z"/></svg>',
'table-row-properties': '<svg width="24" height="24"><path fill-rule="nonzero" d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h14ZM5 15v3h6v-3H5Zm14 0h-6v3h6v-3Zm0-9h-6v3h6V6ZM5 9h6V6H5v3Z"/></svg>',
'table-split-cells': '<svg width="24" height="24"><path fill-rule="nonzero" d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h14ZM8 15.5H5V18h3v-2.5Zm11-5h-9V18h9v-7.5Zm-2.5 1 1 1-2 2 2 2-1 1-2-2-2 2-1-1 2-2-2-2 1-1 2 2 2-2Zm-8.5-1H5v3h3v-3ZM19 6h-4v2.5h4V6ZM8 6H5v2.5h3V6Zm5 0h-3v2.5h3V6Z"/></svg>',
'table-top-header': '<svg width="24" height="24"><path d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h14Zm-8 11H5v3h6v-3Zm8 0h-6v3h6v-3Zm0-5h-6v3h6v-3ZM5 13h6v-3H5v3Z"/></svg>',
'table': '<svg width="24" height="24"><path fill-rule="nonzero" d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h14ZM5 14v4h6v-4H5Zm14 0h-6v4h6v-4Zm0-6h-6v4h6V8ZM5 12h6V8H5v4Z"/></svg>',
'template-add': '<svg width="24" height="24"><path fill-rule="evenodd" clip-rule="evenodd" d="M9 12v4H5a2 2 0 0 0-2 2v3h9.3a6 6 0 0 1-.3-2H5v-1h7a6 6 0 0 1 .8-2H11v-5l-.8-.6a3 3 0 1 1 3.6 0l-.8.6v4.7a6 6 0 0 1 2-1.9V12a5 5 0 1 0-6 0Z"/><path d="M18 15c.5 0 1 .4 1 .9V18h2a1 1 0 0 1 .1 2H19v2a1 1 0 0 1-2 .1V20h-2a1 1 0 0 1-.1-2H17v-2c0-.6.4-1 1-1Z"/></svg>',
'template': '<svg width="24" height="24"><path d="M19 19v-1H5v1h14ZM9 16v-4a5 5 0 1 1 6 0v4h4a2 2 0 0 1 2 2v3H3v-3c0-1.1.9-2 2-2h4Zm4 0v-5l.8-.6a3 3 0 1 0-3.6 0l.8.6v5h2Z" fill-rule="nonzero"/></svg>',
'temporary-placeholder': '<svg width="24" height="24"><g fill-rule="evenodd"><path d="M9 7.6V6h2.5V4.5a.5.5 0 1 1 1 0V6H15v1.6a8 8 0 1 1-6 0Zm-2.6 5.3a.5.5 0 0 0 .3.6c.3 0 .6 0 .6-.3l.1-.2a5 5 0 0 1 3.3-2.8c.3-.1.4-.4.4-.6-.1-.3-.4-.5-.6-.4a6 6 0 0 0-4.1 3.7Z"/><circle cx="14" cy="4" r="1"/><circle cx="12" cy="2" r="1"/><circle cx="10" cy="4" r="1"/></g></svg>',
'text-color': '<svg width="24" height="24"><g fill-rule="evenodd"><path class="tox-icon-text-color__color" d="M3 18h18v3H3z"/><path d="M8.7 16h-.8a.5.5 0 0 1-.5-.6l2.7-9c.1-.3.3-.4.5-.4h2.8c.2 0 .4.1.5.4l2.7 9a.5.5 0 0 1-.5.6h-.8a.5.5 0 0 1-.4-.4l-.7-2.2c0-.3-.3-.4-.5-.4h-3.4c-.2 0-.4.1-.5.4l-.7 2.2c0 .3-.2.4-.4.4Zm2.6-7.6-.6 2a.5.5 0 0 0 .5.6h1.6a.5.5 0 0 0 .5-.6l-.6-2c0-.3-.3-.4-.5-.4h-.4c-.2 0-.4.1-.5.4Z"/></g></svg>',
'text-size-decrease': '<svg width="24" height="24"><path fill-rule="evenodd" clip-rule="evenodd" d="M14 5a1 1 0 1 1 0 2h-4v11a1 1 0 1 1-2 0V7H4a1 1 0 0 1 0-2h10ZM14 12a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2h-6Z"/></svg>',
'text-size-increase': '<svg width="24" height="24"><path fill-rule="evenodd" clip-rule="evenodd" d="M14 5a1 1 0 1 1 0 2h-4v11a1 1 0 1 1-2 0V7H4a1 1 0 0 1 0-2h10ZM17 9a1 1 0 0 0-1 1v2h-2a1 1 0 1 0 0 2h2v2a1 1 0 1 0 2 0v-2h2a1 1 0 1 0 0-2h-2v-2c0-.6-.4-1-1-1Z"/></svg>',
'toc': '<svg width="24" height="24"><path d="M5 5c.6 0 1 .4 1 1s-.4 1-1 1a1 1 0 1 1 0-2Zm3 0h11c.6 0 1 .4 1 1s-.4 1-1 1H8a1 1 0 1 1 0-2Zm-3 8c.6 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1 0-2Zm3 0h11c.6 0 1 .4 1 1s-.4 1-1 1H8a1 1 0 0 1 0-2Zm0-4c.6 0 1 .4 1 1s-.4 1-1 1a1 1 0 1 1 0-2Zm3 0h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm-3 8c.6 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1 0-2Zm3 0h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Z" fill-rule="evenodd"/></svg>',
'transform-image': '<svg height="24" width="24"><path d="M3 21v-6h2V9H3V3h6v2h6V3h6v6h-2v6h2v6h-6v-2H9v2zm6-4h6v-2h2V9h-2V7H9v2H7v6h2zM5 7h2V5H5zm12 0h2V5h-2zm0 12h2v-2h-2zM5 19h2v-2H5z"/></svg>',
'translate': '<svg width="24" height="24"><path d="m12.7 14.3-.3.7-.4.7-2.2-2.2-3.1 3c-.3.4-.8.4-1 0a.7.7 0 0 1 0-1l3.1-3A12.4 12.4 0 0 1 6.7 9H8a10.1 10.1 0 0 0 1.7 2.4c.5-.5 1-1.1 1.4-1.8l.9-2H4.7a.7.7 0 1 1 0-1.5h4.4v-.7c0-.4.3-.8.7-.8.4 0 .7.4.7.8v.7H15c.4 0 .8.3.8.7 0 .4-.4.8-.8.8h-1.4a12.3 12.3 0 0 1-1 2.4 13.5 13.5 0 0 1-1.7 2.3l1.9 1.8Zm4.3-3 2.7 7.3a.5.5 0 0 1-.4.7 1 1 0 0 1-1-.7l-.6-1.5h-3.4l-.6 1.5a1 1 0 0 1-1 .7.5.5 0 0 1-.4-.7l2.7-7.4a1 1 0 0 1 2 0Zm-2.2 4.4h2.4L16 12.5l-1.2 3.2Z" fill-rule="evenodd"/></svg>',
'typography': '<svg width="24" height="24"><path fill-rule="evenodd" clip-rule="evenodd" d="M17 5a1 1 0 1 1 0 2h-4v11a1 1 0 1 1-2 0V7H7a1 1 0 0 1 0-2h10Z"/><path d="m17.5 14 .8-1.7 1.7-.8-1.7-.8-.8-1.7-.8 1.7-1.7.8 1.7.8.8 1.7ZM7 14l1 2 2 1-2 1-1 2-1-2-2-1 2-1 1-2Z"/></svg>',
'underline': '<svg width="24" height="24"><path d="M16 5c.6 0 1 .4 1 1v5.5a4 4 0 0 1-.4 1.8l-1 1.4a5.3 5.3 0 0 1-5.5 1 5 5 0 0 1-1.6-1c-.5-.4-.8-.9-1.1-1.4a4 4 0 0 1-.4-1.8V6c0-.6.4-1 1-1s1 .4 1 1v5.5c0 .3 0 .6.2 1l.6.7a3.3 3.3 0 0 0 2.2.8 3.4 3.4 0 0 0 2.2-.8c.3-.2.4-.5.6-.8l.2-.9V6c0-.6.4-1 1-1ZM8 17h8c.6 0 1 .4 1 1s-.4 1-1 1H8a1 1 0 0 1 0-2Z" fill-rule="evenodd"/></svg>',
'undo': '<svg width="24" height="24"><path d="M6.4 8H12c3.7 0 6.2 2 6.8 5.1.6 2.7-.4 5.6-2.3 6.8a1 1 0 0 1-1-1.8c1.1-.6 1.8-2.7 1.4-4.6-.5-2.1-2.1-3.5-4.9-3.5H6.4l3.3 3.3a1 1 0 1 1-1.4 1.4l-5-5a1 1 0 0 1 0-1.4l5-5a1 1 0 0 1 1.4 1.4L6.4 8Z" fill-rule="nonzero"/></svg>',
'unlink': '<svg width="24" height="24"><path d="M6.2 12.3a1 1 0 0 1 1.4 1.4l-2 2a2 2 0 1 0 2.6 2.8l4.8-4.8a1 1 0 0 0 0-1.4 1 1 0 1 1 1.4-1.3 2.9 2.9 0 0 1 0 4L9.6 20a3.9 3.9 0 0 1-5.5-5.5l2-2Zm11.6-.6a1 1 0 0 1-1.4-1.4l2.1-2a2 2 0 1 0-2.7-2.8L11 10.3a1 1 0 0 0 0 1.4A1 1 0 1 1 9.6 13a2.9 2.9 0 0 1 0-4L14.4 4a3.9 3.9 0 0 1 5.5 5.5l-2 2ZM7.6 6.3a.8.8 0 0 1-1 1.1L3.3 4.2a.7.7 0 1 1 1-1l3.2 3.1ZM5.1 8.6a.8.8 0 0 1 0 1.5H3a.8.8 0 0 1 0-1.5H5Zm5-3.5a.8.8 0 0 1-1.5 0V3a.8.8 0 0 1 1.5 0V5Zm6 11.8a.8.8 0 0 1 1-1l3.2 3.2a.8.8 0 0 1-1 1L16 17Zm-2.2 2a.8.8 0 0 1 1.5 0V21a.8.8 0 0 1-1.5 0V19Zm5-3.5a.7.7 0 1 1 0-1.5H21a.8.8 0 0 1 0 1.5H19Z" fill-rule="nonzero"/></svg>',
'unlock': '<svg width="24" height="24"><path d="M16 5c.8 0 1.5.3 2.1.9.6.6.9 1.3.9 2.1v3h-2V8a1 1 0 0 0-.3-.7A1 1 0 0 0 16 7h-2a1 1 0 0 0-.7.3 1 1 0 0 0-.3.7v3h.3c.2 0 .3 0 .5.2l.2.6v7.4c0 .3 0 .4-.2.6l-.6.2H4.8c-.3 0-.4 0-.6-.2a.7.7 0 0 1-.2-.6v-7.4c0-.3 0-.4.2-.6l.5-.2H11V8c0-.8.3-1.5.9-2.1.6-.6 1.3-.9 2.1-.9h2Z" fill-rule="evenodd"/></svg>',
'unordered-list': '<svg width="24" height="24"><path d="M11 5h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm0 6h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm0 6h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2ZM4.5 6c0-.4.1-.8.4-1 .3-.4.7-.5 1.1-.5.4 0 .8.1 1 .4.4.3.5.7.5 1.1 0 .4-.1.8-.4 1-.3.4-.7.5-1.1.5-.4 0-.8-.1-1-.4-.4-.3-.5-.7-.5-1.1Zm0 6c0-.4.1-.8.4-1 .3-.4.7-.5 1.1-.5.4 0 .8.1 1 .4.4.3.5.7.5 1.1 0 .4-.1.8-.4 1-.3.4-.7.5-1.1.5-.4 0-.8-.1-1-.4-.4-.3-.5-.7-.5-1.1Zm0 6c0-.4.1-.8.4-1 .3-.4.7-.5 1.1-.5.4 0 .8.1 1 .4.4.3.5.7.5 1.1 0 .4-.1.8-.4 1-.3.4-.7.5-1.1.5-.4 0-.8-.1-1-.4-.4-.3-.5-.7-.5-1.1Z" fill-rule="evenodd"/></svg>',
'unpin': '<svg width="24" height="24"><path fill-rule="evenodd" d="M21 19.6 19.7 21l-5-5H13v5h-2v-5H6a1 1 0 0 1-1-1.4l2.4-5.8-.1-.2H6a1 1 0 0 1-1-1V6.5l-2-2L4.4 3 21 19.6ZM7.5 14h5.1L9 10.4 7.5 14Z"/><path d="M18 3a1 1 0 0 1 1 1v3.6a1 1 0 0 1-1 1h-1.5l2.4 6a1 1 0 0 1 0 .2L15.4 11l-1.2-3a1 1 0 0 1 .9-1.4h2V5H9.2l-2-2H18Z"/></svg>',
'unselected': '<svg width="24" height="24"><path fill-rule="nonzero" d="M6 4h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2Zm0 1a1 1 0 0 0-1 1v12c0 .6.4 1 1 1h12c.6 0 1-.4 1-1V6c0-.6-.4-1-1-1H6Z"/></svg>',
'upload-from-device': '<svg width="24" height="24"><path d="M11 18.7c0 .6.4 1 1 1s1-.4 1-1v-3l1.5.8a1 1 0 0 0 1.3-.2 1 1 0 0 0-.3-1.4L12 12.6 8.5 15a1 1 0 0 0-.3 1.4c.3.4.9.5 1.3.2l1.5-.9v3.1Z"/><path d="M9.6 4a1 1 0 0 1 .8.4l1.3 2h6.7A2.6 2.6 0 0 1 21 8.8v8.2a2.6 2.6 0 0 1-2.6 2.6H16v-2h2.4a.6.6 0 0 0 .6-.6V9a.6.6 0 0 0-.6-.6h-7.2a1 1 0 0 1-.8-.4L9 5.9H5.6a.6.6 0 0 0-.6.6v10.6a.6.6 0 0 0 .6.6H8v2H5.6A2.6 2.6 0 0 1 3 17.1V6.5A2.6 2.6 0 0 1 5.6 4h4Z"/></svg>',
'upload-from-link': '<svg height="24" width="24"><path d="M11.9 5.1a4 4 0 0 1 5.5 5.6l-.8.8a1 1 0 0 1-1.4-1.4l.8-.8a2 2 0 0 0-2.7-2.7l-2.4 2.3a3 3 0 0 0-.8 3 1 1 0 0 1-2 .4 5 5 0 0 1 1.4-4.8zm8.7 10.5a1 1 0 0 1-1.2 1.5l-1.4-1V19a1 1 0 0 1-2 0v-3l-1.4 1.1a1 1 0 0 1-1.2-1.5l3.6-2.9 3.6 2.9z"/><path d="M9.7 18.9A4 4 0 0 1 4 13.3l.8-.8A1 1 0 0 1 6.3 14l-.7.8a2 2 0 0 0 0 2.7c.7.8 2.1.9 2.9.1L11 15c.8-.8.7-1.8.5-2.8a1 1 0 0 1 1.9-.5 5 5 0 0 1-1.3 4.8z"/></svg>',
'upload': '<svg width="24" height="24"><path d="M18 19v-2a1 1 0 0 1 2 0v3c0 .6-.4 1-1 1H5a1 1 0 0 1-1-1v-3a1 1 0 0 1 2 0v2h12ZM11 6.4 8.7 8.7a1 1 0 0 1-1.4-1.4l4-4a1 1 0 0 1 1.4 0l4 4a1 1 0 1 1-1.4 1.4L13 6.4V16a1 1 0 0 1-2 0V6.4Z" fill-rule="nonzero"/></svg>',
'user': '<svg width="24" height="24"><path d="M12 24a12 12 0 1 1 0-24 12 12 0 0 1 0 24Zm-8.7-5.3a11 11 0 0 0 17.4 0C19.4 16.3 14.6 15 12 15c-2.6 0-7.4 1.3-8.7 3.7ZM12 13c2.2 0 4-2 4-4.5S14.2 4 12 4 8 6 8 8.5 9.8 13 12 13Z" fill-rule="nonzero"/></svg>',
'vertical-align': '<svg width="24" height="24"><g fill-rule="nonzero"><rect width="18" height="2" x="3" y="11" rx="1"/><path d="M12 2c.6 0 1 .4 1 1v4l2-1.3a1 1 0 0 1 1.2 1.5l-.1.1-4.1 3-4-3a1 1 0 0 1 1-1.7l2 1.5V3c0-.6.4-1 1-1zm0 11.8 4 2.9a1 1 0 0 1-1 1.7l-2-1.5V21c0 .5-.4 1-.9 1H12a1 1 0 0 1-1-1v-4l-2 1.3a1 1 0 0 1-1.2-.1l-.1-.1a1 1 0 0 1 .1-1.3l.1-.1 4.1-3z"/></g></svg>',
'vibrance': '<svg height="24" width="24"><path d="M12 20 22 4H2zm2.6-8 1.3-2H8l1.3 2h5.2zm-1.2 2h-2.8l1.4 2.2zm5-8L17 8H7L5.6 6h12.8z" fill-rule="evenodd"/></svg>',
'visualblocks': '<svg width="24" height="24"><path d="M9 19v2H7v-2h2Zm-4 0v2a2 2 0 0 1-2-2h2Zm8 0v2h-2v-2h2Zm8 0a2 2 0 0 1-2 2v-2h2Zm-4 0v2h-2v-2h2ZM15 7a1 1 0 0 1 0 2v7a1 1 0 0 1-2 0V9h-1v7a1 1 0 0 1-2 0v-4a2.5 2.5 0 0 1-.2-5H15ZM5 15v2H3v-2h2Zm16 0v2h-2v-2h2ZM5 11v2H3v-2h2Zm16 0v2h-2v-2h2ZM5 7v2H3V7h2Zm16 0v2h-2V7h2ZM5 3v2H3c0-1.1.9-2 2-2Zm8 0v2h-2V3h2Zm6 0a2 2 0 0 1 2 2h-2V3ZM9 3v2H7V3h2Zm8 0v2h-2V3h2Z" fill-rule="evenodd"/></svg>',
'visualchars': '<svg width="24" height="24"><path d="M10 5h7a1 1 0 0 1 0 2h-1v11a1 1 0 0 1-2 0V7h-2v11a1 1 0 0 1-2 0v-6c-.5 0-1 0-1.4-.3A3.4 3.4 0 0 1 6.8 10a3.3 3.3 0 0 1 0-2.8 3.4 3.4 0 0 1 1.8-1.8L10 5Z" fill-rule="evenodd"/></svg>',
'vk': '<svg width="24" height="24"><path fill-rule="evenodd" d="M14.8 17.4a2 2 0 0 1-1.3.7 8 8 0 0 1-7.7-3.6c-1.4-2-2.2-4.4-2.6-5.7L3 8.6A2 2 0 0 1 5 6h2.5c.4 0 .8.1 1.2.4A2 2 0 0 1 9.8 6h3.5a2 2 0 0 1 1.4.6 2 2 0 0 1 1.5-.6h2.4a2 2 0 0 1 2 2.5c-.5 1.5-1.2 2.6-1.8 3.6a12.5 12.5 0 0 1 2 3.2A2 2 0 0 1 19 18h-2.6a2 2 0 0 1-1.3-.5 6.9 6.9 0 0 1-.3-.2Zm.5-2.5-1.2-1a3 3 0 0 0-.8-.2v2.4h-1.7a5.6 5.6 0 0 1-.3 0c-3-.7-4.7-3.7-5.6-6.1A38.1 38.1 0 0 1 5 8.1V8h2.5a80.5 80.5 0 0 1 1.2 2.5c.6 1.2 1.2 2 2 2.1V9l-.9-1h3.5v4.1a5 5 0 0 0 .4-.3c.7-.7 1.2-1.4 1.6-2.2l.2-.3.7-1.3h2.4a8.5 8.5 0 0 1-.8 2l-1.3 2-.3.3a10.5 10.5 0 0 1 2.8 3.8h-2.6l-.8-.9a20.6 20.6 0 0 0-.3-.3Z" clip-rule="evenodd"/></svg>',
'warmth': '<svg height="24" width="24"><path d="M12 21.1c-1.4 0-2.6-.5-3.6-1.5s-1.5-2.1-1.5-3.5c0-.8.2-1.6.6-2.2A5 5 0 0 1 8.9 12v-6c0-.8.3-1.5 1-2.1a2.9 2.9 0 0 1 2-.9c.9 0 1.6.3 2.2.9.5.6.8 1.3.8 2.1v6a5 5 0 0 1 1.5 1.8A4.9 4.9 0 0 1 17 16c0 1.4-.5 2.6-1.4 3.5s-2.2 1.5-3.6 1.5zm-3-5h6c0-.5-.2-1-.4-1.4a3 3 0 0 0-.9-1l-.8-.6v-7a1 1 0 0 0-.3-.7A1 1 0 0 0 12 5a1 1 0 0 0-.7.3 1 1 0 0 0-.3.7v7l-.8.6a2.9 2.9 0 0 0-.9 1 3 3 0 0 0-.3 1.4z"/></svg>',
'warning': '<svg width="24" height="24"><path d="M19.8 18.3c.2.5.3.9 0 1.2-.1.3-.5.5-1 .5H5.2c-.5 0-.9-.2-1-.5-.3-.3-.2-.7 0-1.2L11 4.7l.5-.5.5-.2c.2 0 .3 0 .5.2.2 0 .3.3.5.5l6.8 13.6ZM12 18c.3 0 .5-.1.7-.3.2-.2.3-.4.3-.7a1 1 0 0 0-.3-.7 1 1 0 0 0-.7-.3 1 1 0 0 0-.7.3 1 1 0 0 0-.3.7c0 .3.1.5.3.7.2.2.4.3.7.3Zm.7-3 .3-4a1 1 0 0 0-.3-.7 1 1 0 0 0-.7-.3 1 1 0 0 0-.7.3 1 1 0 0 0-.3.7l.3 4h1.4Z" fill-rule="evenodd"/></svg>',
'web-search': '<svg width="24" height="24"><path fill-rule="evenodd" d="M12 3a9 9 0 1 1 0 18 9 9 0 0 1 0-18ZM5 13a7 7 0 0 0 4.8 5.6l-.2-.2A13.3 13.3 0 0 1 8 13H5Zm11 0a13.3 13.3 0 0 1-1.6 5.3c0 .2-.1.2-.2.3A7 7 0 0 0 19 13h-3Zm-6 0a11.3 11.3 0 0 0 1.4 4.4l.6 1 .6-1A11.3 11.3 0 0 0 14 13h-4Zm-.2-7.6A7 7 0 0 0 5 11h3a13.3 13.3 0 0 1 1.5-5.3l.2-.3Zm2.2.2a11.3 11.3 0 0 0-2 5.4h4a11.3 11.3 0 0 0-2-5.4Zm2.4 0A13.3 13.3 0 0 1 16 11h3a7 7 0 0 0-4.8-5.6l.2.3Z"/></svg>',
'zoom-in': '<svg width="24" height="24"><path d="M16 17.3a8 8 0 1 1 1.4-1.4l4.3 4.4a1 1 0 0 1-1.4 1.4l-4.4-4.3Zm-5-.3a6 6 0 1 0 0-12 6 6 0 0 0 0 12Zm-1-9a1 1 0 0 1 2 0v6a1 1 0 0 1-2 0V8Zm-2 4a1 1 0 0 1 0-2h6a1 1 0 0 1 0 2H8Z" fill-rule="nonzero"/></svg>',
'zoom-out': '<svg width="24" height="24"><path d="M16 17.3a8 8 0 1 1 1.4-1.4l4.3 4.4a1 1 0 0 1-1.4 1.4l-4.4-4.3Zm-5-.3a6 6 0 1 0 0-12 6 6 0 0 0 0 12Zm-3-5a1 1 0 0 1 0-2h6a1 1 0 0 1 0 2H8Z" fill-rule="nonzero"/></svg>',
}
});
File diff suppressed because one or more lines are too long
-7
View File
@@ -1,7 +0,0 @@
// Exports the "default" icons for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/icons/default')
// ES2015:
// import 'tinymce/icons/default'
require('./icons.js');
+3
View File
@@ -0,0 +1,3 @@
This is where language files should be placed.
Please DO NOT translate these directly, use this service instead: https://crowdin.com/project/tinymce
+4 -7
View File
@@ -1,9 +1,6 @@
Copyright (c) 2025 Ephox Corporation DBA Tiny Technologies, Inc.
# Software License Agreement
SOFTWARE TERMS & CONDITIONS OF USE
**TinyMCE** – [<https://github.com/tinymce/tinymce>](https://github.com/tinymce/tinymce)
Copyright (c) 2024, Ephox Corporation DBA Tiny Technologies, Inc.
Licensed under, and subject to the restrictions of:
1. The terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html); or
2. The Tiny Technologies, Inc Software Terms & Conditions of Use presented at <https://about.tiny.cloud/legal/> and specifically the Tiny Self-Hosted License Agreement, unless you have signed a negotiated commercial license agreement with us in which case your use is governed by those terms. By use of this Software you have agreed to these Tiny Technologies, Inc Software Terms & Conditions of Use.
If you are uncertain about your licensing requirements, please contact <sales@tiny.cloud> for further clarification.
Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html).
-7
View File
@@ -1,7 +0,0 @@
// Exports the "dom" model for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/models/dom')
// ES2015:
// import 'tinymce/models/dom'
require('./model.js');
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-21
View File
@@ -1,21 +0,0 @@
Below is a list of third party libraries that this software uses:
----------------------------------------------------------------
dompurify
owner: Mario Heiderich
repo: https://github.com/cure53/DOMPurify
version: 3.3.2
license: MPL-2.0 OR Apache-2.0
prismjs
owner: Lea Verou
repo: https://github.com/PrismJS/prism
version: 1.25.0
license: MIT
prism-themes
owner: Lea Verou
repo: https://github.com/PrismJS/prism-themes
version: 1.9.0
license: MIT
-32
View File
@@ -1,32 +0,0 @@
{
"name": "tinymce",
"version": "8.5.1",
"repository": {
"type": "git",
"url": "https://github.com/tinymce/tinymce.git",
"directory": "modules/tinymce"
},
"description": "Web based JavaScript HTML WYSIWYG editor control.",
"author": "Ephox Corporation DBA Tiny Technologies, Inc",
"main": "tinymce.js",
"types": "tinymce.d.ts",
"license": "SEE LICENSE IN license.md",
"keywords": [
"wysiwyg",
"tinymce",
"richtext",
"javascript",
"html",
"text",
"rich editor",
"rich text editor",
"rte",
"rich text",
"contenteditable",
"editing"
],
"homepage": "https://www.tiny.cloud/",
"bugs": {
"url": "https://github.com/tinymce/tinymce/issues"
}
}
-7
View File
@@ -1,7 +0,0 @@
// Exports the "accordion" plugin for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/plugins/accordion')
// ES2015:
// import 'tinymce/plugins/accordion'
require('./plugin.js');
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-7
View File
@@ -1,7 +0,0 @@
// Exports the "advlist" plugin for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/plugins/advlist')
// ES2015:
// import 'tinymce/plugins/advlist'
require('./plugin.js');
-473
View File
@@ -1,473 +0,0 @@
/**
* TinyMCE version 8.5.1 (2026-05-19)
*/
(function () {
'use strict';
var global$1 = tinymce.util.Tools.resolve('tinymce.PluginManager');
const applyListFormat = (editor, listName, styleValue) => {
const cmd = listName === 'UL' ? 'InsertUnorderedList' : 'InsertOrderedList';
editor.execCommand(cmd, false, styleValue === false ? null : { 'list-style-type': styleValue });
};
const register$2 = (editor) => {
editor.addCommand('ApplyUnorderedListStyle', (ui, value) => {
applyListFormat(editor, 'UL', value['list-style-type']);
});
editor.addCommand('ApplyOrderedListStyle', (ui, value) => {
applyListFormat(editor, 'OL', value['list-style-type']);
});
};
const option = (name) => (editor) => editor.options.get(name);
const register$1 = (editor) => {
const registerOption = editor.options.register;
registerOption('advlist_number_styles', {
processor: 'string[]',
default: 'default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman'.split(',')
});
registerOption('advlist_bullet_styles', {
processor: 'string[]',
default: 'default,disc,circle,square'.split(',')
});
};
const getNumberStyles = option('advlist_number_styles');
const getBulletStyles = option('advlist_bullet_styles');
/* eslint-disable @typescript-eslint/no-wrapper-object-types */
const isNullable = (a) => a === null || a === undefined;
const isNonNullable = (a) => !isNullable(a);
/**
* The `Optional` type represents a value (of any type) that potentially does
* not exist. Any `Optional<T>` can either be a `Some<T>` (in which case the
* value does exist) or a `None` (in which case the value does not exist). This
* module defines a whole lot of FP-inspired utility functions for dealing with
* `Optional` objects.
*
* Comparison with null or undefined:
* - We don't get fancy null coalescing operators with `Optional`
* - We do get fancy helper functions with `Optional`
* - `Optional` support nesting, and allow for the type to still be nullable (or
* another `Optional`)
* - There is no option to turn off strict-optional-checks like there is for
* strict-null-checks
*/
class Optional {
tag;
value;
// Sneaky optimisation: every instance of Optional.none is identical, so just
// reuse the same object
static singletonNone = new Optional(false);
// The internal representation has a `tag` and a `value`, but both are
// private: able to be console.logged, but not able to be accessed by code
constructor(tag, value) {
this.tag = tag;
this.value = value;
}
// --- Identities ---
/**
* Creates a new `Optional<T>` that **does** contain a value.
*/
static some(value) {
return new Optional(true, value);
}
/**
* Create a new `Optional<T>` that **does not** contain a value. `T` can be
* any type because we don't actually have a `T`.
*/
static none() {
return Optional.singletonNone;
}
/**
* Perform a transform on an `Optional` type. Regardless of whether this
* `Optional` contains a value or not, `fold` will return a value of type `U`.
* If this `Optional` does not contain a value, the `U` will be created by
* calling `onNone`. If this `Optional` does contain a value, the `U` will be
* created by calling `onSome`.
*
* For the FP enthusiasts in the room, this function:
* 1. Could be used to implement all of the functions below
* 2. Forms a catamorphism
*/
fold(onNone, onSome) {
if (this.tag) {
return onSome(this.value);
}
else {
return onNone();
}
}
/**
* Determine if this `Optional` object contains a value.
*/
isSome() {
return this.tag;
}
/**
* Determine if this `Optional` object **does not** contain a value.
*/
isNone() {
return !this.tag;
}
// --- Functor (name stolen from Haskell / maths) ---
/**
* Perform a transform on an `Optional` object, **if** there is a value. If
* you provide a function to turn a T into a U, this is the function you use
* to turn an `Optional<T>` into an `Optional<U>`. If this **does** contain
* a value then the output will also contain a value (that value being the
* output of `mapper(this.value)`), and if this **does not** contain a value
* then neither will the output.
*/
map(mapper) {
if (this.tag) {
return Optional.some(mapper(this.value));
}
else {
return Optional.none();
}
}
// --- Monad (name stolen from Haskell / maths) ---
/**
* Perform a transform on an `Optional` object, **if** there is a value.
* Unlike `map`, here the transform itself also returns an `Optional`.
*/
bind(binder) {
if (this.tag) {
return binder(this.value);
}
else {
return Optional.none();
}
}
// --- Traversable (name stolen from Haskell / maths) ---
/**
* For a given predicate, this function finds out if there **exists** a value
* inside this `Optional` object that meets the predicate. In practice, this
* means that for `Optional`s that do not contain a value it returns false (as
* no predicate-meeting value exists).
*/
exists(predicate) {
return this.tag && predicate(this.value);
}
/**
* For a given predicate, this function finds out if **all** the values inside
* this `Optional` object meet the predicate. In practice, this means that
* for `Optional`s that do not contain a value it returns true (as all 0
* objects do meet the predicate).
*/
forall(predicate) {
return !this.tag || predicate(this.value);
}
filter(predicate) {
if (!this.tag || predicate(this.value)) {
return this;
}
else {
return Optional.none();
}
}
// --- Getters ---
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided `Optional` object does not contain a
* value.
*/
getOr(replacement) {
return this.tag ? this.value : replacement;
}
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided `Optional` object does not contain a
* value. Unlike `getOr`, in this method the `replacement` object is also
* `Optional` - meaning that this method will always return an `Optional`.
*/
or(replacement) {
return this.tag ? this : replacement;
}
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided `Optional` object does not contain a
* value. Unlike `getOr`, in this method the `replacement` value is
* "thunked" - that is to say that you don't pass a value to `getOrThunk`, you
* pass a function which (if called) will **return** the `value` you want to
* use.
*/
getOrThunk(thunk) {
return this.tag ? this.value : thunk();
}
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided Optional object does not contain a
* value.
*
* Unlike `or`, in this method the `replacement` value is "thunked" - that is
* to say that you don't pass a value to `orThunk`, you pass a function which
* (if called) will **return** the `value` you want to use.
*
* Unlike `getOrThunk`, in this method the `replacement` value is also
* `Optional`, meaning that this method will always return an `Optional`.
*/
orThunk(thunk) {
return this.tag ? this : thunk();
}
/**
* Get the value out of the inside of the `Optional` object, throwing an
* exception if the provided `Optional` object does not contain a value.
*
* WARNING:
* You should only be using this function if you know that the `Optional`
* object **is not** empty (otherwise you're throwing exceptions in production
* code, which is bad).
*
* In tests this is more acceptable.
*
* Prefer other methods to this, such as `.each`.
*/
getOrDie(message) {
if (!this.tag) {
throw new Error(message ?? 'Called getOrDie on None');
}
else {
return this.value;
}
}
// --- Interop with null and undefined ---
/**
* Creates an `Optional` value from a nullable (or undefined-able) input.
* Null, or undefined, is converted to `None`, and anything else is converted
* to `Some`.
*/
static from(value) {
return isNonNullable(value) ? Optional.some(value) : Optional.none();
}
/**
* Converts an `Optional` to a nullable type, by getting the value if it
* exists, or returning `null` if it does not.
*/
getOrNull() {
return this.tag ? this.value : null;
}
/**
* Converts an `Optional` to an undefined-able type, by getting the value if
* it exists, or returning `undefined` if it does not.
*/
getOrUndefined() {
return this.value;
}
// --- Utilities ---
/**
* If the `Optional` contains a value, perform an action on that value.
* Unlike the rest of the methods on this type, `.each` has side-effects. If
* you want to transform an `Optional<T>` **into** something, then this is not
* the method for you. If you want to use an `Optional<T>` to **do**
* something, then this is the method for you - provided you're okay with not
* doing anything in the case where the `Optional` doesn't have a value inside
* it. If you're not sure whether your use-case fits into transforming
* **into** something or **doing** something, check whether it has a return
* value. If it does, you should be performing a transform.
*/
each(worker) {
if (this.tag) {
worker(this.value);
}
}
/**
* Turn the `Optional` object into an array that contains all of the values
* stored inside the `Optional`. In practice, this means the output will have
* either 0 or 1 elements.
*/
toArray() {
return this.tag ? [this.value] : [];
}
/**
* Turn the `Optional` object into a string for debugging or printing. Not
* recommended for production code, but good for debugging. Also note that
* these days an `Optional` object can be logged to the console directly, and
* its inner value (if it exists) will be visible.
*/
toString() {
return this.tag ? `some(${this.value})` : 'none()';
}
}
const nativeIndexOf = Array.prototype.indexOf;
const rawIndexOf = (ts, t) => nativeIndexOf.call(ts, t);
const contains = (xs, x) => rawIndexOf(xs, x) > -1;
const findUntil = (xs, pred, until) => {
for (let i = 0, len = xs.length; i < len; i++) {
const x = xs[i];
if (pred(x, i)) {
return Optional.some(x);
}
else if (until(x, i)) {
break;
}
}
return Optional.none();
};
// There are many variations of Object iteration that are faster than the 'for-in' style:
// http://jsperf.com/object-keys-iteration/107
//
// Use the native keys if it is available (IE9+), otherwise fall back to manually filtering
const keys = Object.keys;
const each = (obj, f) => {
const props = keys(obj);
for (let k = 0, len = props.length; k < len; k++) {
const i = props[k];
const x = obj[i];
f(x, i);
}
};
const map = (obj, f) => {
return tupleMap(obj, (x, i) => ({
k: i,
v: f(x, i)
}));
};
const tupleMap = (obj, f) => {
const r = {};
each(obj, (x, i) => {
const tuple = f(x, i);
r[tuple.k] = tuple.v;
});
return r;
};
var global = tinymce.util.Tools.resolve('tinymce.util.Tools');
const isCustomList = (list) => /\btox\-/.test(list.className);
const isChildOfBody = (editor, elm) => {
return editor.dom.isChildOf(elm, editor.getBody());
};
const matchNodeNames = (regex) => (node) => isNonNullable(node) && regex.test(node.nodeName);
const isListNode = matchNodeNames(/^(OL|UL|DL)$/);
const isTableCellNode = matchNodeNames(/^(TH|TD)$/);
const inList = (editor, parents, nodeName) => findUntil(parents, (parent) => isListNode(parent) && !isCustomList(parent), isTableCellNode)
.exists((list) => list.nodeName === nodeName && isChildOfBody(editor, list));
const getSelectedStyleType = (editor) => {
const listElm = editor.dom.getParent(editor.selection.getNode(), 'ol,ul');
const style = editor.dom.getStyle(listElm, 'listStyleType');
return Optional.from(style);
};
// Lists/core/Util.ts - Duplicated in Lists plugin
const isWithinNonEditable = (editor, element) => element !== null && !editor.dom.isEditable(element);
const isWithinNonEditableList = (editor, element) => {
const parentList = editor.dom.getParent(element, 'ol,ul,dl');
return isWithinNonEditable(editor, parentList) || !editor.selection.isEditable();
};
const setNodeChangeHandler = (editor, nodeChangeHandler) => {
const initialNode = editor.selection.getNode();
// Set the initial state
nodeChangeHandler({
parents: editor.dom.getParents(initialNode),
element: initialNode
});
editor.on('NodeChange', nodeChangeHandler);
return () => editor.off('NodeChange', nodeChangeHandler);
};
// <ListStyles>
const styleValueToText = (styleValue) => {
return styleValue.replace(/\-/g, ' ').replace(/\b\w/g, (chr) => {
return chr.toUpperCase();
});
};
const normalizeStyleValue = (styleValue) => isNullable(styleValue) || styleValue === 'default' ? '' : styleValue;
const makeSetupHandler = (editor, nodeName) => (api) => {
const updateButtonState = (editor, parents) => {
const element = editor.selection.getStart(true);
api.setActive(inList(editor, parents, nodeName));
api.setEnabled(!isWithinNonEditableList(editor, element));
};
const nodeChangeHandler = (e) => updateButtonState(editor, e.parents);
return setNodeChangeHandler(editor, nodeChangeHandler);
};
const addSplitButton = (editor, id, tooltip, cmd, nodeName, styles) => {
const listStyleTypeAliases = {
'lower-latin': 'lower-alpha',
'upper-latin': 'upper-alpha',
'lower-alpha': 'lower-latin',
'upper-alpha': 'upper-latin'
};
const stylesContainsAliasMap = map(listStyleTypeAliases, (alias) => contains(styles, alias));
editor.ui.registry.addSplitButton(id, {
tooltip,
chevronTooltip: tooltip,
icon: nodeName === "OL" /* ListType.OrderedList */ ? 'ordered-list' : 'unordered-list',
presets: 'listpreview',
columns: nodeName === "OL" /* ListType.OrderedList */ ? 3 : 4,
fetch: (callback) => {
const items = global.map(styles, (styleValue) => {
const iconStyle = nodeName === "OL" /* ListType.OrderedList */ ? 'num' : 'bull';
const iconName = styleValue === 'decimal' ? 'default' : styleValue;
const itemValue = normalizeStyleValue(styleValue);
const displayText = styleValueToText(styleValue);
return {
type: 'choiceitem',
value: itemValue,
icon: 'list-' + iconStyle + '-' + iconName,
text: displayText
};
});
callback(items);
},
onAction: () => editor.execCommand(cmd),
onItemAction: (_splitButtonApi, value) => {
applyListFormat(editor, nodeName, value);
},
select: (value) => {
const listStyleType = getSelectedStyleType(editor);
return listStyleType.exists((listStyle) => value === listStyle || (listStyleTypeAliases[listStyle] === value && !stylesContainsAliasMap[value]));
},
onSetup: makeSetupHandler(editor, nodeName)
});
};
const addButton = (editor, id, tooltip, cmd, nodeName, styleValue) => {
editor.ui.registry.addToggleButton(id, {
active: false,
tooltip,
icon: nodeName === "OL" /* ListType.OrderedList */ ? 'ordered-list' : 'unordered-list',
onSetup: makeSetupHandler(editor, nodeName),
// Need to make sure the button removes rather than applies if a list of the same type is selected
onAction: () => editor.queryCommandState(cmd) || styleValue === '' ? editor.execCommand(cmd) : applyListFormat(editor, nodeName, styleValue)
});
};
const addControl = (editor, id, tooltip, cmd, nodeName, styles) => {
if (styles.length > 1) {
addSplitButton(editor, id, tooltip, cmd, nodeName, styles);
}
else {
addButton(editor, id, tooltip, cmd, nodeName, normalizeStyleValue(styles[0]));
}
};
const register = (editor) => {
addControl(editor, 'numlist', 'Numbered list', 'InsertOrderedList', "OL" /* ListType.OrderedList */, getNumberStyles(editor));
addControl(editor, 'bullist', 'Bullet list', 'InsertUnorderedList', "UL" /* ListType.UnorderedList */, getBulletStyles(editor));
};
var Plugin = () => {
global$1.add('advlist', (editor) => {
if (editor.hasPlugin('lists')) {
register$1(editor);
register(editor);
register$2(editor);
}
else {
// eslint-disable-next-line no-console
console.error('Please use the Lists plugin together with the List Styles plugin.');
}
});
};
Plugin();
/** *****
* DO NOT EXPORT ANYTHING
*
* IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE
*******/
})();
+4 -1
View File
@@ -1 +1,4 @@
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=(t,e,s)=>{const r="UL"===e?"InsertUnorderedList":"InsertOrderedList";t.execCommand(r,!1,!1===s?null:{"list-style-type":s})},s=t=>e=>e.options.get(t),r=s("advlist_number_styles"),n=s("advlist_bullet_styles"),l=t=>null==t,i=t=>!l(t);class o{tag;value;static singletonNone=new o(!1);constructor(t,e){this.tag=t,this.value=e}static some(t){return new o(!0,t)}static none(){return o.singletonNone}fold(t,e){return this.tag?e(this.value):t()}isSome(){return this.tag}isNone(){return!this.tag}map(t){return this.tag?o.some(t(this.value)):o.none()}bind(t){return this.tag?t(this.value):o.none()}exists(t){return this.tag&&t(this.value)}forall(t){return!this.tag||t(this.value)}filter(t){return!this.tag||t(this.value)?this:o.none()}getOr(t){return this.tag?this.value:t}or(t){return this.tag?this:t}getOrThunk(t){return this.tag?this.value:t()}orThunk(t){return this.tag?this:t()}getOrDie(t){if(this.tag)return this.value;throw new Error(t??"Called getOrDie on None")}static from(t){return i(t)?o.some(t):o.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(t){this.tag&&t(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}const a=Array.prototype.indexOf,u=Object.keys;var d=tinymce.util.Tools.resolve("tinymce.util.Tools");const c=t=>e=>i(e)&&t.test(e.nodeName),h=c(/^(OL|UL|DL)$/),g=c(/^(TH|TD)$/),p=t=>l(t)||"default"===t?"":t,m=(t,e)=>s=>((t,e)=>{const s=t.selection.getNode();return e({parents:t.dom.getParents(s),element:s}),t.on("NodeChange",e),()=>t.off("NodeChange",e)})(t,r=>((t,r)=>{const n=t.selection.getStart(!0);s.setActive(((t,e,s)=>((t,e,s)=>{for(let r=0,n=t.length;r<n;r++){const n=t[r];if(e(n))return o.some(n);if(s(n,r))break}return o.none()})(e,t=>h(t)&&!/\btox\-/.test(t.className),g).exists(e=>e.nodeName===s&&((t,e)=>t.dom.isChildOf(e,t.getBody()))(t,e)))(t,r,e)),s.setEnabled(!((t,e)=>{const s=t.dom.getParent(e,"ol,ul,dl");return((t,e)=>null!==e&&!t.dom.isEditable(e))(t,s)||!t.selection.isEditable()})(t,n))})(t,r.parents)),v=(t,s,r,n,l,i)=>{const c={"lower-latin":"lower-alpha","upper-latin":"upper-alpha","lower-alpha":"lower-latin","upper-alpha":"upper-latin"},h=(g=t=>{return e=i,s=t,a.call(e,s)>-1;var e,s},((t,e)=>{const s={};return((t,e)=>{const s=u(t);for(let r=0,n=s.length;r<n;r++){const n=s[r];e(t[n],n)}})(t,(t,r)=>{const n=e(t,r);s[n.k]=n.v}),s})(c,(t,e)=>({k:e,v:g(t)})));var g;t.ui.registry.addSplitButton(s,{tooltip:r,chevronTooltip:r,icon:"OL"===l?"ordered-list":"unordered-list",presets:"listpreview",columns:"OL"===l?3:4,fetch:t=>{t(d.map(i,t=>{const e="OL"===l?"num":"bull",s="decimal"===t?"default":t,r=p(t),n=(t=>t.replace(/\-/g," ").replace(/\b\w/g,t=>t.toUpperCase()))(t);return{type:"choiceitem",value:r,icon:"list-"+e+"-"+s,text:n}}))},onAction:()=>t.execCommand(n),onItemAction:(s,r)=>{e(t,l,r)},select:e=>{const s=(t=>{const e=t.dom.getParent(t.selection.getNode(),"ol,ul"),s=t.dom.getStyle(e,"listStyleType");return o.from(s)})(t);return s.exists(t=>e===t||c[t]===e&&!h[e])},onSetup:m(t,l)})},y=(t,s,r,n,l,i)=>{i.length>1?v(t,s,r,n,l,i):((t,s,r,n,l,i)=>{t.ui.registry.addToggleButton(s,{active:!1,tooltip:r,icon:"OL"===l?"ordered-list":"unordered-list",onSetup:m(t,l),onAction:()=>t.queryCommandState(n)||""===i?t.execCommand(n):e(t,l,i)})})(t,s,r,n,l,p(i[0]))};t.add("advlist",t=>{t.hasPlugin("lists")?((t=>{const e=t.options.register;e("advlist_number_styles",{processor:"string[]",default:"default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman".split(",")}),e("advlist_bullet_styles",{processor:"string[]",default:"default,disc,circle,square".split(",")})})(t),(t=>{y(t,"numlist","Numbered list","InsertOrderedList","OL",r(t)),y(t,"bullist","Bullet list","InsertUnorderedList","UL",n(t))})(t),(t=>{t.addCommand("ApplyUnorderedListStyle",(s,r)=>{e(t,"UL",r["list-style-type"])}),t.addCommand("ApplyOrderedListStyle",(s,r)=>{e(t,"OL",r["list-style-type"])})})(t)):console.error("Please use the Lists plugin together with the List Styles plugin.")})}();
/**
* TinyMCE version 7.3.0 (2024-08-07)
*/
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=(t,e,s)=>{const r="UL"===e?"InsertUnorderedList":"InsertOrderedList";t.execCommand(r,!1,!1===s?null:{"list-style-type":s})},s=t=>e=>e.options.get(t),r=s("advlist_number_styles"),n=s("advlist_bullet_styles"),i=t=>null==t,l=t=>!i(t);var o=tinymce.util.Tools.resolve("tinymce.util.Tools");class a{constructor(t,e){this.tag=t,this.value=e}static some(t){return new a(!0,t)}static none(){return a.singletonNone}fold(t,e){return this.tag?e(this.value):t()}isSome(){return this.tag}isNone(){return!this.tag}map(t){return this.tag?a.some(t(this.value)):a.none()}bind(t){return this.tag?t(this.value):a.none()}exists(t){return this.tag&&t(this.value)}forall(t){return!this.tag||t(this.value)}filter(t){return!this.tag||t(this.value)?this:a.none()}getOr(t){return this.tag?this.value:t}or(t){return this.tag?this:t}getOrThunk(t){return this.tag?this.value:t()}orThunk(t){return this.tag?this:t()}getOrDie(t){if(this.tag)return this.value;throw new Error(null!=t?t:"Called getOrDie on None")}static from(t){return l(t)?a.some(t):a.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(t){this.tag&&t(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}a.singletonNone=new a(!1);const u=t=>e=>l(e)&&t.test(e.nodeName),d=u(/^(OL|UL|DL)$/),g=u(/^(TH|TD)$/),c=t=>i(t)||"default"===t?"":t,h=(t,e)=>s=>((t,e)=>{const s=t.selection.getNode();return e({parents:t.dom.getParents(s),element:s}),t.on("NodeChange",e),()=>t.off("NodeChange",e)})(t,(r=>((t,r)=>{const n=t.selection.getStart(!0);s.setActive(((t,e,s)=>((t,e,s)=>{for(let e=0,n=t.length;e<n;e++){const n=t[e];if(d(r=n)&&!/\btox\-/.test(r.className))return a.some(n);if(s(n,e))break}var r;return a.none()})(e,0,g).exists((e=>e.nodeName===s&&((t,e)=>t.dom.isChildOf(e,t.getBody()))(t,e))))(t,r,e)),s.setEnabled(!((t,e)=>{const s=t.dom.getParent(e,"ol,ul,dl");return((t,e)=>null!==e&&!t.dom.isEditable(e))(t,s)&&t.selection.isEditable()})(t,n)&&t.selection.isEditable())})(t,r.parents))),m=(t,s,r,n,i,l)=>{l.length>1?((t,s,r,n,i,l)=>{t.ui.registry.addSplitButton(s,{tooltip:r,icon:"OL"===i?"ordered-list":"unordered-list",presets:"listpreview",columns:3,fetch:t=>{t(o.map(l,(t=>{const e="OL"===i?"num":"bull",s="disc"===t||"decimal"===t?"default":t,r=c(t),n=(t=>t.replace(/\-/g," ").replace(/\b\w/g,(t=>t.toUpperCase())))(t);return{type:"choiceitem",value:r,icon:"list-"+e+"-"+s,text:n}})))},onAction:()=>t.execCommand(n),onItemAction:(s,r)=>{e(t,i,r)},select:e=>{const s=(t=>{const e=t.dom.getParent(t.selection.getNode(),"ol,ul"),s=t.dom.getStyle(e,"listStyleType");return a.from(s)})(t);return s.map((t=>e===t)).getOr(!1)},onSetup:h(t,i)})})(t,s,r,n,i,l):((t,s,r,n,i,l)=>{t.ui.registry.addToggleButton(s,{active:!1,tooltip:r,icon:"OL"===i?"ordered-list":"unordered-list",onSetup:h(t,i),onAction:()=>t.queryCommandState(n)||""===l?t.execCommand(n):e(t,i,l)})})(t,s,r,n,i,c(l[0]))};t.add("advlist",(t=>{t.hasPlugin("lists")?((t=>{const e=t.options.register;e("advlist_number_styles",{processor:"string[]",default:"default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman".split(",")}),e("advlist_bullet_styles",{processor:"string[]",default:"default,circle,square".split(",")})})(t),(t=>{m(t,"numlist","Numbered list","InsertOrderedList","OL",r(t)),m(t,"bullist","Bullet list","InsertUnorderedList","UL",n(t))})(t),(t=>{t.addCommand("ApplyUnorderedListStyle",((s,r)=>{e(t,"UL",r["list-style-type"])})),t.addCommand("ApplyOrderedListStyle",((s,r)=>{e(t,"OL",r["list-style-type"])}))})(t)):console.error("Please use the Lists plugin together with the List Styles plugin.")}))}();
-7
View File
@@ -1,7 +0,0 @@
// Exports the "anchor" plugin for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/plugins/anchor')
// ES2015:
// import 'tinymce/plugins/anchor'
require('./plugin.js');
-237
View File
@@ -1,237 +0,0 @@
/**
* TinyMCE version 8.5.1 (2026-05-19)
*/
(function () {
'use strict';
var global$2 = tinymce.util.Tools.resolve('tinymce.PluginManager');
var global$1 = tinymce.util.Tools.resolve('tinymce.dom.RangeUtils');
var global = tinymce.util.Tools.resolve('tinymce.util.Tools');
const option = (name) => (editor) => editor.options.get(name);
const register$2 = (editor) => {
const registerOption = editor.options.register;
registerOption('allow_html_in_named_anchor', {
processor: 'boolean',
default: false
});
};
const allowHtmlInNamedAnchor = option('allow_html_in_named_anchor');
const namedAnchorSelector = 'a:not([href])';
const isEmptyString = (str) => !str;
const getIdFromAnchor = (elm) => {
const id = elm.getAttribute('id') || elm.getAttribute('name');
return id || '';
};
const isAnchor = (elm) => elm.nodeName.toLowerCase() === 'a';
const isNamedAnchor = (elm) => isAnchor(elm) && !elm.getAttribute('href') && getIdFromAnchor(elm) !== '';
const isEmptyNamedAnchor = (elm) => isNamedAnchor(elm) && !elm.firstChild;
const removeEmptyNamedAnchorsInSelection = (editor) => {
const dom = editor.dom;
global$1(dom).walk(editor.selection.getRng(), (nodes) => {
global.each(nodes, (node) => {
if (isEmptyNamedAnchor(node)) {
dom.remove(node, false);
}
});
});
};
const isValidId = (id) =>
// Follows HTML4 rules: https://www.w3.org/TR/html401/types.html#type-id
/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(id);
const getNamedAnchor = (editor) => editor.dom.getParent(editor.selection.getStart(), namedAnchorSelector);
const getId = (editor) => {
const anchor = getNamedAnchor(editor);
if (anchor) {
return getIdFromAnchor(anchor);
}
else {
return '';
}
};
const createAnchor = (editor, id) => {
editor.undoManager.transact(() => {
if (!allowHtmlInNamedAnchor(editor)) {
editor.selection.collapse(true);
}
if (editor.selection.isCollapsed()) {
editor.insertContent(editor.dom.createHTML('a', { id }));
}
else {
// Remove any empty named anchors in the selection as they cannot be removed by the formatter since they are cef
removeEmptyNamedAnchorsInSelection(editor);
// Format is set up to truncate any partially selected named anchors so that they are not completely removed
editor.formatter.remove('namedAnchor', undefined, undefined, true);
// Insert new anchor using the formatter - will wrap selected content in anchor
editor.formatter.apply('namedAnchor', { value: id });
// Need to add visual classes to anchors if required
editor.addVisual();
}
});
};
const updateAnchor = (editor, id, anchorElement) => {
anchorElement.removeAttribute('name');
anchorElement.id = id;
editor.addVisual(); // Need to add visual classes to anchors if required
editor.undoManager.add();
};
const insert = (editor, id) => {
const anchor = getNamedAnchor(editor);
if (anchor) {
updateAnchor(editor, id, anchor);
}
else {
createAnchor(editor, id);
}
editor.focus();
};
const insertAnchor = (editor, newId) => {
if (!isValidId(newId)) {
editor.windowManager.alert('ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.');
return false;
}
else {
insert(editor, newId);
return true;
}
};
const open = (editor) => {
const currentId = getId(editor);
editor.windowManager.open({
title: 'Anchor',
size: 'normal',
body: {
type: 'panel',
items: [
{
name: 'id',
type: 'input',
label: 'ID',
placeholder: 'example'
}
]
},
buttons: [
{
type: 'cancel',
name: 'cancel',
text: 'Cancel'
},
{
type: 'submit',
name: 'save',
text: 'Save',
primary: true
}
],
initialData: {
id: currentId
},
onSubmit: (api) => {
if (insertAnchor(editor, api.getData().id)) { // TODO we need a better way to do validation
api.close();
}
}
});
};
const register$1 = (editor) => {
editor.addCommand('mceAnchor', () => {
open(editor);
});
};
// Note: node.firstChild check is for the 'allow_html_in_named_anchor' setting
// Only want to add contenteditable attributes if there is no text within the anchor
const isNamedAnchorNode = (node) => isEmptyString(node.attr('href')) && !isEmptyString(node.attr('id') || node.attr('name'));
const isEmptyNamedAnchorNode = (node) => isNamedAnchorNode(node) && !node.firstChild;
const setContentEditable = (state) => (nodes) => {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (isEmptyNamedAnchorNode(node)) {
node.attr('contenteditable', state);
}
}
};
const setup = (editor) => {
editor.on('PreInit', () => {
editor.parser.addNodeFilter('a', setContentEditable('false'));
editor.serializer.addNodeFilter('a', setContentEditable(null));
});
};
const registerFormats = (editor) => {
editor.formatter.register('namedAnchor', {
inline: 'a',
selector: namedAnchorSelector,
remove: 'all',
split: true,
deep: true,
attributes: {
id: '%value'
},
onmatch: (node, _fmt, _itemName) => {
return isNamedAnchor(node);
}
});
};
const onSetupEditable = (editor) => (api) => {
const nodeChanged = () => {
api.setEnabled(editor.selection.isEditable());
};
editor.on('NodeChange', nodeChanged);
nodeChanged();
return () => {
editor.off('NodeChange', nodeChanged);
};
};
const register = (editor) => {
const onAction = () => editor.execCommand('mceAnchor');
editor.ui.registry.addToggleButton('anchor', {
icon: 'bookmark',
tooltip: 'Anchor',
onAction,
onSetup: (buttonApi) => {
const unbindSelectorChanged = editor.selection.selectorChangedWithUnbind('a:not([href])', buttonApi.setActive).unbind;
const unbindEditableChanged = onSetupEditable(editor)(buttonApi);
return () => {
unbindSelectorChanged();
unbindEditableChanged();
};
}
});
editor.ui.registry.addMenuItem('anchor', {
icon: 'bookmark',
text: 'Anchor...',
onAction,
onSetup: onSetupEditable(editor)
});
};
var Plugin = () => {
global$2.add('anchor', (editor) => {
register$2(editor);
setup(editor);
register$1(editor);
register(editor);
editor.on('PreInit', () => {
registerFormats(editor);
});
});
};
Plugin();
/** *****
* DO NOT EXPORT ANYTHING
*
* IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE
*******/
})();
+4 -1
View File
@@ -1 +1,4 @@
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),o=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=e=>e.options.get("allow_html_in_named_anchor");const a="a:not([href])",r=e=>!e,i=e=>e.getAttribute("id")||e.getAttribute("name")||"",l=e=>(e=>"a"===e.nodeName.toLowerCase())(e)&&!e.getAttribute("href")&&""!==i(e),s=e=>e.dom.getParent(e.selection.getStart(),a),d=(e,a)=>{const r=s(e);r?((e,t,o)=>{o.removeAttribute("name"),o.id=t,e.addVisual(),e.undoManager.add()})(e,a,r):((e,a)=>{e.undoManager.transact(()=>{n(e)||e.selection.collapse(!0),e.selection.isCollapsed()?e.insertContent(e.dom.createHTML("a",{id:a})):((e=>{const n=e.dom;t(n).walk(e.selection.getRng(),e=>{o.each(e,e=>{var t;l(t=e)&&!t.firstChild&&n.remove(e,!1)})})})(e),e.formatter.remove("namedAnchor",void 0,void 0,!0),e.formatter.apply("namedAnchor",{value:a}),e.addVisual())})})(e,a),e.focus()},c=e=>(e=>r(e.attr("href"))&&!r(e.attr("id")||e.attr("name")))(e)&&!e.firstChild,m=e=>t=>{for(let o=0;o<t.length;o++){const n=t[o];c(n)&&n.attr("contenteditable",e)}},u=e=>t=>{const o=()=>{t.setEnabled(e.selection.isEditable())};return e.on("NodeChange",o),o(),()=>{e.off("NodeChange",o)}};e.add("anchor",e=>{(e=>{(0,e.options.register)("allow_html_in_named_anchor",{processor:"boolean",default:!1})})(e),(e=>{e.on("PreInit",()=>{e.parser.addNodeFilter("a",m("false")),e.serializer.addNodeFilter("a",m(null))})})(e),(e=>{e.addCommand("mceAnchor",()=>{(e=>{const t=(e=>{const t=s(e);return t?i(t):""})(e);e.windowManager.open({title:"Anchor",size:"normal",body:{type:"panel",items:[{name:"id",type:"input",label:"ID",placeholder:"example"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{id:t},onSubmit:t=>{((e,t)=>/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(t)?(d(e,t),!0):(e.windowManager.alert("ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores."),!1))(e,t.getData().id)&&t.close()}})})(e)})})(e),(e=>{const t=()=>e.execCommand("mceAnchor");e.ui.registry.addToggleButton("anchor",{icon:"bookmark",tooltip:"Anchor",onAction:t,onSetup:t=>{const o=e.selection.selectorChangedWithUnbind("a:not([href])",t.setActive).unbind,n=u(e)(t);return()=>{o(),n()}}}),e.ui.registry.addMenuItem("anchor",{icon:"bookmark",text:"Anchor...",onAction:t,onSetup:u(e)})})(e),e.on("PreInit",()=>{(e=>{e.formatter.register("namedAnchor",{inline:"a",selector:a,remove:"all",split:!0,deep:!0,attributes:{id:"%value"},onmatch:(e,t,o)=>l(e)})})(e)})})}();
/**
* TinyMCE version 7.3.0 (2024-08-07)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),o=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=("allow_html_in_named_anchor",e=>e.options.get("allow_html_in_named_anchor"));const a="a:not([href])",r=e=>!e,i=e=>e.getAttribute("id")||e.getAttribute("name")||"",l=e=>(e=>"a"===e.nodeName.toLowerCase())(e)&&!e.getAttribute("href")&&""!==i(e),s=e=>e.dom.getParent(e.selection.getStart(),a),d=(e,a)=>{const r=s(e);r?((e,t,o)=>{o.removeAttribute("name"),o.id=t,e.addVisual(),e.undoManager.add()})(e,a,r):((e,a)=>{e.undoManager.transact((()=>{n(e)||e.selection.collapse(!0),e.selection.isCollapsed()?e.insertContent(e.dom.createHTML("a",{id:a})):((e=>{const n=e.dom;t(n).walk(e.selection.getRng(),(e=>{o.each(e,(e=>{var t;l(t=e)&&!t.firstChild&&n.remove(e,!1)}))}))})(e),e.formatter.remove("namedAnchor",void 0,void 0,!0),e.formatter.apply("namedAnchor",{value:a}),e.addVisual())}))})(e,a),e.focus()},c=e=>(e=>r(e.attr("href"))&&!r(e.attr("id")||e.attr("name")))(e)&&!e.firstChild,m=e=>t=>{for(let o=0;o<t.length;o++){const n=t[o];c(n)&&n.attr("contenteditable",e)}},u=e=>t=>{const o=()=>{t.setEnabled(e.selection.isEditable())};return e.on("NodeChange",o),o(),()=>{e.off("NodeChange",o)}};e.add("anchor",(e=>{(e=>{(0,e.options.register)("allow_html_in_named_anchor",{processor:"boolean",default:!1})})(e),(e=>{e.on("PreInit",(()=>{e.parser.addNodeFilter("a",m("false")),e.serializer.addNodeFilter("a",m(null))}))})(e),(e=>{e.addCommand("mceAnchor",(()=>{(e=>{const t=(e=>{const t=s(e);return t?i(t):""})(e);e.windowManager.open({title:"Anchor",size:"normal",body:{type:"panel",items:[{name:"id",type:"input",label:"ID",placeholder:"example"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{id:t},onSubmit:t=>{((e,t)=>/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(t)?(d(e,t),!0):(e.windowManager.alert("ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores."),!1))(e,t.getData().id)&&t.close()}})})(e)}))})(e),(e=>{const t=()=>e.execCommand("mceAnchor");e.ui.registry.addToggleButton("anchor",{icon:"bookmark",tooltip:"Anchor",onAction:t,onSetup:t=>{const o=e.selection.selectorChangedWithUnbind("a:not([href])",t.setActive).unbind,n=u(e)(t);return()=>{o(),n()}}}),e.ui.registry.addMenuItem("anchor",{icon:"bookmark",text:"Anchor...",onAction:t,onSetup:u(e)})})(e),e.on("PreInit",(()=>{(e=>{e.formatter.register("namedAnchor",{inline:"a",selector:a,remove:"all",split:!0,deep:!0,attributes:{id:"%value"},onmatch:(e,t,o)=>l(e)})})(e)}))}))}();
-7
View File
@@ -1,7 +0,0 @@
// Exports the "autolink" plugin for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/plugins/autolink')
// ES2015:
// import 'tinymce/plugins/autolink'
require('./plugin.js');
-315
View File
@@ -1,315 +0,0 @@
/**
* TinyMCE version 8.5.1 (2026-05-19)
*/
(function () {
'use strict';
var global$1 = tinymce.util.Tools.resolve('tinymce.PluginManager');
/* eslint-disable @typescript-eslint/no-wrapper-object-types */
const hasProto = (v, constructor, predicate) => {
if (predicate(v, constructor.prototype)) {
return true;
}
else {
// String-based fallback time
return v.constructor?.name === constructor.name;
}
};
const typeOf = (x) => {
const t = typeof x;
if (x === null) {
return 'null';
}
else if (t === 'object' && Array.isArray(x)) {
return 'array';
}
else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) {
return 'string';
}
else {
return t;
}
};
const isType = (type) => (value) => typeOf(value) === type;
const eq = (t) => (a) => t === a;
const isString = isType('string');
const isUndefined = eq(undefined);
const isNullable = (a) => a === null || a === undefined;
const isNonNullable = (a) => !isNullable(a);
const not = (f) => (t) => !f(t);
const hasOwnProperty = Object.hasOwnProperty;
const has = (obj, key) => hasOwnProperty.call(obj, key);
const checkRange = (str, substr, start) => substr === '' || str.length >= substr.length && str.substr(start, start + substr.length) === substr;
const contains = (str, substr, start = 0, end) => {
const idx = str.indexOf(substr, start);
if (idx !== -1) {
return isUndefined(end) ? true : idx + substr.length <= end;
}
else {
return false;
}
};
/** Does 'str' start with 'prefix'?
* Note: all strings start with the empty string.
* More formally, for all strings x, startsWith(x, "").
* This is so that for all strings x and y, startsWith(y + x, y)
*/
const startsWith = (str, prefix) => {
return checkRange(str, prefix, 0);
};
const zeroWidth = '\uFEFF';
const isZwsp = (char) => char === zeroWidth;
const removeZwsp = (s) => s.replace(/\uFEFF/g, '');
/*
The RegEx parses the following components (https://www.rfc-editor.org/rfc/rfc3986.txt):
scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]
foo://example.com:8042/over/there?name=ferret#nose
\_/ \______________/\_________/ \_________/ \__/
| | | | |
scheme authority path query fragment
Originally from:
http://blog.mattheworiordan.com/post/13174566389/url-regular-expression-for-links-with-or-without-the
Modified to:
- include port numbers
- allow full stops in email addresses
- allow [-.~*+=!&;:'%@?^${}(),\/\w] after the #
- allow [-.~*+=!&;:'%@?^${}(),\/\w] after the ?
- move allow -_.~*+=!&;:'%@?^${}() in email usernames to the first @ match (TBIO-4809)
- enforce domains to be [A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)* so they can't end in a period (TBIO-4809)
- removed a bunch of escaping, made every group non-capturing (during TBIO-4809)
- colons are only valid when followed directly by // or some text and then @ (TBIO-4867)
- only include the fragment '#' if it has 1 or more trailing matches
- only include the query '?' if it has 1 or more trailing matches
- allow commas in URL path
- exclude trailing comma and period in URL path
- allow up to 15 character schemes including all valid characters from the spec https://url.spec.whatwg.org/#url-scheme-string (TINY-5074)
- changed instances of 0-9 to be \d (TINY-5074)
- reduced duplication (TINY-5074)
- allow [*!;:'@$] in the path segment as they are valid characters per the spec: https://url.spec.whatwg.org/#url-path-segment-string (TINY-8069)
(?:
(?:
[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?
| www\.
| [-;:&=+$,.\w]+@
)
[A-Za-z\d-]+
(?:\.[A-Za-z\d-]+)*
)
(?::\d+)?
(?:
\/
(?:
[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w]
)?
)?
(?:
\?
(?:
[-.~*+=!&;:'%@?^${}(),\/\w]+
)
)?
(?:
#
(?:
[-.~*+=!&;:'%@?^${}(),\/\w]+
)
)?
*/
const link = () =>
// eslint-disable-next-line max-len
/(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)[A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?/g;
const option = (name) => (editor) => editor.options.get(name);
const register = (editor) => {
const registerOption = editor.options.register;
registerOption('autolink_pattern', {
processor: 'regexp',
// Use the Polaris link detection, however for autolink we need to make it be an exact match
default: new RegExp('^' + link().source + '$', 'i')
});
registerOption('link_default_target', {
processor: 'string'
});
registerOption('link_default_protocol', {
processor: 'string',
default: 'https'
});
};
const getAutoLinkPattern = option('autolink_pattern');
const getDefaultLinkTarget = option('link_default_target');
const getDefaultLinkProtocol = option('link_default_protocol');
const allowUnsafeLinkTarget = option('allow_unsafe_link_target');
var global = tinymce.util.Tools.resolve('tinymce.dom.TextSeeker');
const isTextNode = (node) => node.nodeType === 3;
const isElement = (node) => node.nodeType === 1;
const isBracketOrSpace = (char) => /^[(\[{ \u00a0]$/.test(char);
// Note: This is similar to the Polaris protocol detection, except it also handles `mailto` and any length scheme
const hasProtocol = (url) => /^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(url);
// A limited list of punctuation characters that might be used after a link
const isPunctuation = (char) => /[?!,.;:]/.test(char);
const findChar = (text, index, predicate) => {
for (let i = index - 1; i >= 0; i--) {
const char = text.charAt(i);
if (!isZwsp(char) && predicate(char)) {
return i;
}
}
return -1;
};
const freefallRtl = (container, offset) => {
let tempNode = container;
let tempOffset = offset;
while (isElement(tempNode) && tempNode.childNodes[tempOffset]) {
tempNode = tempNode.childNodes[tempOffset];
tempOffset = isTextNode(tempNode) ? tempNode.data.length : tempNode.childNodes.length;
}
return { container: tempNode, offset: tempOffset };
};
const parseCurrentLine = (editor, offset) => {
const voidElements = editor.schema.getVoidElements();
const autoLinkPattern = getAutoLinkPattern(editor);
const { dom, selection } = editor;
// Never create a link when we are inside a link
if (dom.getParent(selection.getNode(), 'a[href]') !== null || editor.mode.isReadOnly()) {
return null;
}
const rng = selection.getRng();
const textSeeker = global(dom, (node) => {
return dom.isBlock(node) || has(voidElements, node.nodeName.toLowerCase()) || dom.getContentEditable(node) === 'false' || dom.getParent(node, 'a[href]') !== null;
});
// Descend down the end container to find the text node
const { container: endContainer, offset: endOffset } = freefallRtl(rng.endContainer, rng.endOffset);
// Find the root container to use when walking
const root = dom.getParent(endContainer, dom.isBlock) ?? dom.getRoot();
// Move the selection backwards to the start of the potential URL to account for the pressed character
// while also excluding the last full stop from a word like "www.site.com."
const endSpot = textSeeker.backwards(endContainer, endOffset + offset, (node, offset) => {
const text = node.data;
const idx = findChar(text, offset, not(isBracketOrSpace));
// Move forward one so the offset is after the found character unless the found char is a punctuation char
return idx === -1 || isPunctuation(text[idx]) ? idx : idx + 1;
}, root);
if (!endSpot) {
return null;
}
// Walk backwards until we find a boundary or a bracket/space
let lastTextNode = endSpot.container;
const startSpot = textSeeker.backwards(endSpot.container, endSpot.offset, (node, offset) => {
lastTextNode = node;
const idx = findChar(node.data, offset, isBracketOrSpace);
// Move forward one so that the offset is after the bracket/space
return idx === -1 ? idx : idx + 1;
}, root);
const newRng = dom.createRng();
if (!startSpot) {
newRng.setStart(lastTextNode, 0);
}
else {
newRng.setStart(startSpot.container, startSpot.offset);
}
newRng.setEnd(endSpot.container, endSpot.offset);
const rngText = removeZwsp(newRng.toString());
const matches = rngText.match(autoLinkPattern);
if (matches) {
let url = matches[0];
if (startsWith(url, 'www.')) {
const protocol = getDefaultLinkProtocol(editor);
url = protocol + '://' + url;
}
else if (contains(url, '@') && !hasProtocol(url)) {
url = 'mailto:' + url;
}
return { rng: newRng, url };
}
else {
return null;
}
};
const convertToLink = (editor, result) => {
const { dom, selection } = editor;
const { rng, url } = result;
const bookmark = selection.getBookmark();
selection.setRng(rng);
// Needs to be a native createlink command since this is executed in a keypress event handler
// so the pending character that is to be inserted needs to be inserted after the link. That will not
// happen if we use the formatter create link version. Since we're using the native command
// then we also need to ensure the exec command events are fired for backwards compatibility.
const command = 'createlink';
const args = { command, ui: false, value: url };
const beforeExecEvent = editor.dispatch('BeforeExecCommand', args);
if (!beforeExecEvent.isDefaultPrevented()) {
editor.getDoc().execCommand(command, false, url);
editor.dispatch('ExecCommand', args);
const defaultLinkTarget = getDefaultLinkTarget(editor);
if (isString(defaultLinkTarget)) {
const anchor = selection.getNode();
dom.setAttrib(anchor, 'target', defaultLinkTarget);
// Ensure noopener is added for blank targets to prevent window opener attacks
if (defaultLinkTarget === '_blank' && !allowUnsafeLinkTarget(editor)) {
dom.setAttrib(anchor, 'rel', 'noopener');
}
}
}
selection.moveToBookmark(bookmark);
editor.nodeChanged();
};
const handleSpacebar = (editor) => {
const result = parseCurrentLine(editor, -1);
if (isNonNullable(result)) {
convertToLink(editor, result);
}
};
const handleBracket = handleSpacebar;
const handleEnter = (editor) => {
const result = parseCurrentLine(editor, 0);
if (isNonNullable(result)) {
convertToLink(editor, result);
}
};
const setup = (editor) => {
editor.on('keydown', (e) => {
if (e.keyCode === 13 && !e.isDefaultPrevented()) {
handleEnter(editor);
}
});
editor.on('keyup', (e) => {
if (e.keyCode === 32) {
handleSpacebar(editor);
// One of the closing bracket keys: ), ] or }
}
else if (e.keyCode === 48 && e.shiftKey || e.keyCode === 221) {
handleBracket(editor);
}
});
};
var Plugin = () => {
global$1.add('autolink', (editor) => {
register(editor);
setup(editor);
});
};
Plugin();
/** *****
* DO NOT EXPORT ANYTHING
*
* IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE
*******/
})();
+4 -1
View File
@@ -1 +1,4 @@
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>"string"===(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(n=o=e,(r=String).prototype.isPrototypeOf(n)||o.constructor?.name===r.name)?"string":t;var n,o,r})(e);const n=e=>undefined===e;const o=e=>!(e=>null==e)(e),r=Object.hasOwnProperty,a=e=>"\ufeff"===e,s=e=>t=>t.options.get(e),l=s("autolink_pattern"),c=s("link_default_target"),i=s("link_default_protocol"),d=s("allow_unsafe_link_target");var u=tinymce.util.Tools.resolve("tinymce.dom.TextSeeker");const f=e=>3===e.nodeType,g=e=>1===e.nodeType,m=e=>/^[(\[{ \u00a0]$/.test(e),k=(e,t,n)=>{for(let o=t-1;o>=0;o--){const t=e.charAt(o);if(!a(t)&&n(t))return o}return-1},p=(e,t)=>{const o=e.schema.getVoidElements(),a=l(e),{dom:s,selection:c}=e;if(null!==s.getParent(c.getNode(),"a[href]")||e.mode.isReadOnly())return null;const d=c.getRng(),p=u(s,e=>{return s.isBlock(e)||(t=o,n=e.nodeName.toLowerCase(),r.call(t,n))||"false"===s.getContentEditable(e)||null!==s.getParent(e,"a[href]");var t,n}),{container:y,offset:w}=((e,t)=>{let n=e,o=t;for(;g(n)&&n.childNodes[o];)n=n.childNodes[o],o=f(n)?n.data.length:n.childNodes.length;return{container:n,offset:o}})(d.endContainer,d.endOffset),h=s.getParent(y,s.isBlock)??s.getRoot(),_=p.backwards(y,w+t,(e,t)=>{const n=e.data,o=k(n,t,(r=m,e=>!r(e)));var r,a;return-1===o||(a=n[o],/[?!,.;:]/.test(a))?o:o+1},h);if(!_)return null;let v=_.container;const A=p.backwards(_.container,_.offset,(e,t)=>{v=e;const n=k(e.data,t,m);return-1===n?n:n+1},h),C=s.createRng();A?C.setStart(A.container,A.offset):C.setStart(v,0),C.setEnd(_.container,_.offset);const b=C.toString().replace(/\uFEFF/g,"").match(a);if(b){let t=b[0];return $="www.",(P=t).length>=4&&P.substr(0,4)===$?t=i(e)+"://"+t:((e,t,o=0,r)=>{const a=e.indexOf(t,o);return-1!==a&&(!!n(r)||a+t.length<=r)})(t,"@")&&!(e=>/^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(e))(t)&&(t="mailto:"+t),{rng:C,url:t}}var P,$;return null},y=(e,n)=>{const{dom:o,selection:r}=e,{rng:a,url:s}=n,l=r.getBookmark();r.setRng(a);const i="createlink",u={command:i,ui:!1,value:s};if(!e.dispatch("BeforeExecCommand",u).isDefaultPrevented()){e.getDoc().execCommand(i,!1,s),e.dispatch("ExecCommand",u);const n=c(e);if(t(n)){const t=r.getNode();o.setAttrib(t,"target",n),"_blank"!==n||d(e)||o.setAttrib(t,"rel","noopener")}}r.moveToBookmark(l),e.nodeChanged()},w=e=>{const t=p(e,-1);o(t)&&y(e,t)},h=w;e.add("autolink",e=>{(e=>{const t=e.options.register;t("autolink_pattern",{processor:"regexp",default:new RegExp("^"+/(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)[A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?/g.source+"$","i")}),t("link_default_target",{processor:"string"}),t("link_default_protocol",{processor:"string",default:"https"})})(e),(e=>{e.on("keydown",t=>{13!==t.keyCode||t.isDefaultPrevented()||(e=>{const t=p(e,0);o(t)&&y(e,t)})(e)}),e.on("keyup",t=>{32===t.keyCode?w(e):(48===t.keyCode&&t.shiftKey||221===t.keyCode)&&h(e)})})(e)})}();
/**
* TinyMCE version 7.3.0 (2024-08-07)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>t.options.get(e),n=t("autolink_pattern"),o=t("link_default_target"),r=t("link_default_protocol"),a=t("allow_unsafe_link_target"),s=("string",e=>"string"===(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(n=o=e,(r=String).prototype.isPrototypeOf(n)||(null===(a=o.constructor)||void 0===a?void 0:a.name)===r.name)?"string":t;var n,o,r,a})(e));const l=(void 0,e=>undefined===e);const i=e=>!(e=>null==e)(e),c=Object.hasOwnProperty,d=e=>"\ufeff"===e;var u=tinymce.util.Tools.resolve("tinymce.dom.TextSeeker");const f=e=>/^[(\[{ \u00a0]$/.test(e),g=(e,t,n)=>{for(let o=t-1;o>=0;o--){const t=e.charAt(o);if(!d(t)&&n(t))return o}return-1},m=(e,t)=>{var o;const a=e.schema.getVoidElements(),s=n(e),{dom:i,selection:d}=e;if(null!==i.getParent(d.getNode(),"a[href]"))return null;const m=d.getRng(),k=u(i,(e=>{return i.isBlock(e)||(t=a,n=e.nodeName.toLowerCase(),c.call(t,n))||"false"===i.getContentEditable(e);var t,n})),{container:p,offset:y}=((e,t)=>{let n=e,o=t;for(;1===n.nodeType&&n.childNodes[o];)n=n.childNodes[o],o=3===n.nodeType?n.data.length:n.childNodes.length;return{container:n,offset:o}})(m.endContainer,m.endOffset),w=null!==(o=i.getParent(p,i.isBlock))&&void 0!==o?o:i.getRoot(),h=k.backwards(p,y+t,((e,t)=>{const n=e.data,o=g(n,t,(r=f,e=>!r(e)));var r,a;return-1===o||(a=n[o],/[?!,.;:]/.test(a))?o:o+1}),w);if(!h)return null;let v=h.container;const _=k.backwards(h.container,h.offset,((e,t)=>{v=e;const n=g(e.data,t,f);return-1===n?n:n+1}),w),A=i.createRng();_?A.setStart(_.container,_.offset):A.setStart(v,0),A.setEnd(h.container,h.offset);const C=A.toString().replace(/\uFEFF/g,"").match(s);if(C){let t=C[0];return $="www.",(b=t).length>=4&&b.substr(0,4)===$?t=r(e)+"://"+t:((e,t,n=0,o)=>{const r=e.indexOf(t,n);return-1!==r&&(!!l(o)||r+t.length<=o)})(t,"@")&&!(e=>/^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(e))(t)&&(t="mailto:"+t),{rng:A,url:t}}var b,$;return null},k=(e,t)=>{const{dom:n,selection:r}=e,{rng:l,url:i}=t,c=r.getBookmark();r.setRng(l);const d="createlink",u={command:d,ui:!1,value:i};if(!e.dispatch("BeforeExecCommand",u).isDefaultPrevented()){e.getDoc().execCommand(d,!1,i),e.dispatch("ExecCommand",u);const t=o(e);if(s(t)){const o=r.getNode();n.setAttrib(o,"target",t),"_blank"!==t||a(e)||n.setAttrib(o,"rel","noopener")}}r.moveToBookmark(c),e.nodeChanged()},p=e=>{const t=m(e,-1);i(t)&&k(e,t)},y=p;e.add("autolink",(e=>{(e=>{const t=e.options.register;t("autolink_pattern",{processor:"regexp",default:new RegExp("^"+/(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)[A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?/g.source+"$","i")}),t("link_default_target",{processor:"string"}),t("link_default_protocol",{processor:"string",default:"https"})})(e),(e=>{e.on("keydown",(t=>{13!==t.keyCode||t.isDefaultPrevented()||(e=>{const t=m(e,0);i(t)&&k(e,t)})(e)})),e.on("keyup",(t=>{32===t.keyCode?p(e):(48===t.keyCode&&t.shiftKey||221===t.keyCode)&&y(e)}))})(e)}))}();
-7
View File
@@ -1,7 +0,0 @@
// Exports the "autoresize" plugin for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/plugins/autoresize')
// ES2015:
// import 'tinymce/plugins/autoresize'
require('./plugin.js');
-221
View File
@@ -1,221 +0,0 @@
/**
* TinyMCE version 8.5.1 (2026-05-19)
*/
(function () {
'use strict';
const Cell = (initial) => {
let value = initial;
const get = () => {
return value;
};
const set = (v) => {
value = v;
};
return {
get,
set
};
};
var global$1 = tinymce.util.Tools.resolve('tinymce.PluginManager');
var global = tinymce.util.Tools.resolve('tinymce.Env');
const fireResizeEditor = (editor) => editor.dispatch('ResizeEditor');
const option = (name) => (editor) => editor.options.get(name);
const register$1 = (editor) => {
const registerOption = editor.options.register;
registerOption('autoresize_overflow_padding', {
processor: 'number',
default: 1
});
registerOption('autoresize_bottom_margin', {
processor: 'number',
default: 50
});
};
const getMinHeight = option('min_height');
const getMaxHeight = option('max_height');
const getAutoResizeOverflowPadding = option('autoresize_overflow_padding');
const getAutoResizeBottomMargin = option('autoresize_bottom_margin');
/**
* This class contains all core logic for the autoresize plugin.
*
* @class tinymce.autoresize.Plugin
* @private
*/
const isFullscreen = (editor) => editor.plugins.fullscreen && editor.plugins.fullscreen.isFullscreen();
const toggleScrolling = (editor, state) => {
const body = editor.getBody();
if (body) {
body.style.overflowY = state ? '' : 'hidden';
if (!state) {
body.scrollTop = 0;
}
}
};
const parseCssValueToInt = (dom, elm, name, computed) => {
const value = parseInt(dom.getStyle(elm, name, computed) ?? '', 10);
// The value maybe be an empty string, so in that case treat it as being 0
return isNaN(value) ? 0 : value;
};
const shouldScrollIntoView = (trigger) => {
// Only scroll the selection into view when we're inserting content. Any other
// triggers the selection should already be in view and resizing would only
// extend the content area.
if (trigger?.type.toLowerCase() === 'setcontent') {
const setContentEvent = trigger;
return setContentEvent.selection === true || setContentEvent.paste === true;
}
else {
return false;
}
};
/**
* This method gets executed each time the editor needs to resize.
*/
const resize = (editor, oldSize, trigger, getExtraMarginBottom) => {
const dom = editor.dom;
const doc = editor.getDoc();
if (!doc) {
return;
}
if (isFullscreen(editor)) {
toggleScrolling(editor, true);
return;
}
const docEle = doc.documentElement;
const resizeBottomMargin = getExtraMarginBottom ? getExtraMarginBottom() : getAutoResizeOverflowPadding(editor);
const minHeight = getMinHeight(editor) ?? editor.getElement().offsetHeight;
let resizeHeight = minHeight;
// Calculate outer height of the doc element using CSS styles
const marginTop = parseCssValueToInt(dom, docEle, 'margin-top', true);
const marginBottom = parseCssValueToInt(dom, docEle, 'margin-bottom', true);
let contentHeight = docEle.offsetHeight + marginTop + marginBottom + resizeBottomMargin;
// Make sure we have a valid height
if (contentHeight < 0) {
contentHeight = 0;
}
// Determine the size of the chroming (menubar, toolbar, etc...)
const containerHeight = editor.getContainer().offsetHeight;
const contentAreaHeight = editor.getContentAreaContainer().offsetHeight;
const chromeHeight = containerHeight - contentAreaHeight;
// Don't make it smaller than the minimum height
if (contentHeight + chromeHeight > minHeight) {
resizeHeight = contentHeight + chromeHeight;
}
// If a maximum height has been defined don't exceed this height
const maxHeight = getMaxHeight(editor);
if (maxHeight && resizeHeight > maxHeight) {
resizeHeight = maxHeight;
toggleScrolling(editor, true);
}
else {
toggleScrolling(editor, false);
}
const old = oldSize.get();
if (old.set) {
editor.dom.setStyles(editor.getDoc().documentElement, { 'min-height': 0 });
editor.dom.setStyles(editor.getBody(), { 'min-height': 'inherit' });
}
// Resize content element
if (resizeHeight !== old.totalHeight && (contentHeight - resizeBottomMargin !== old.contentHeight || !old.set)) {
const deltaSize = (resizeHeight - old.totalHeight);
dom.setStyle(editor.getContainer(), 'height', resizeHeight + 'px');
oldSize.set({
totalHeight: resizeHeight,
contentHeight,
set: true,
});
fireResizeEditor(editor);
// iPadOS has an issue where it won't rerender the body when the iframe is resized
// however if we reset the scroll position then it re-renders correctly
if (global.browser.isSafari() && (global.os.isMacOS() || global.os.isiOS())) {
const win = editor.getWin();
win.scrollTo(win.pageXOffset, win.pageYOffset);
}
// Ensure the selection is in view, as it's potentially out of view after inserting content into the editor
if (editor.hasFocus() && shouldScrollIntoView(trigger)) {
editor.selection.scrollIntoView();
}
// WebKit doesn't decrease the size of the body element until the iframe gets resized
// So we need to continue to resize the iframe down until the size gets fixed
if ((global.browser.isSafari() || global.browser.isChromium()) && deltaSize < 0) {
resize(editor, oldSize, trigger, getExtraMarginBottom);
}
}
};
const setup = (editor, oldSize) => {
const getExtraMarginBottom = () => getAutoResizeBottomMargin(editor);
editor.on('init', (e) => {
const overflowPadding = getAutoResizeOverflowPadding(editor);
const dom = editor.dom;
// Disable height 100% on the root document element otherwise we'll end up resizing indefinitely
dom.setStyles(editor.getDoc().documentElement, {
height: 'auto'
});
if (global.browser.isEdge() || global.browser.isIE()) {
dom.setStyles(editor.getBody(), {
'paddingLeft': overflowPadding,
'paddingRight': overflowPadding,
// IE & Edge have a min height of 150px by default on the body, so override that
'min-height': 0
});
}
else {
dom.setStyles(editor.getBody(), {
paddingLeft: overflowPadding,
paddingRight: overflowPadding
});
}
resize(editor, oldSize, e, getExtraMarginBottom);
});
editor.on('NodeChange SetContent keyup FullscreenStateChanged ResizeContent', (e) => {
resize(editor, oldSize, e, getExtraMarginBottom);
});
};
const register = (editor, oldSize) => {
editor.addCommand('mceAutoResize', () => {
resize(editor, oldSize);
});
};
/**
* This class contains all core logic for the autoresize plugin.
*
* @class tinymce.autoresize.Plugin
* @private
*/
var Plugin = () => {
global$1.add('autoresize', (editor) => {
register$1(editor);
// If autoresize is enabled, disable resize if the user hasn't explicitly enabled it
// TINY-8288: This currently does nothing because of a bug in the theme
if (!editor.options.isSet('resize')) {
editor.options.set('resize', false);
}
if (!editor.inline) {
const oldSize = Cell({
totalHeight: 0,
contentHeight: 0,
set: false,
});
register(editor, oldSize);
setup(editor, oldSize);
}
});
};
Plugin();
/** *****
* DO NOT EXPORT ANYTHING
*
* IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE
*******/
})();
+4 -1
View File
@@ -1 +1,4 @@
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.Env");const o=e=>t=>t.options.get(e),s=o("min_height"),n=o("max_height"),i=o("autoresize_overflow_padding"),r=o("autoresize_bottom_margin"),g=(e,t)=>{const o=e.getBody();o&&(o.style.overflowY=t?"":"hidden",t||(o.scrollTop=0))},l=(e,t,o,s)=>{const n=parseInt(e.getStyle(t,o,s)??"",10);return isNaN(n)?0:n},a=(e,o,r,c)=>{const d=e.dom,h=e.getDoc();if(!h)return;if((e=>e.plugins.fullscreen&&e.plugins.fullscreen.isFullscreen())(e))return void g(e,!0);const m=h.documentElement,u=c?c():i(e),f=s(e)??e.getElement().offsetHeight;let p=f;const y=l(d,m,"margin-top",!0),S=l(d,m,"margin-bottom",!0);let C=m.offsetHeight+y+S+u;C<0&&(C=0);const H=e.getContainer().offsetHeight-e.getContentAreaContainer().offsetHeight;C+H>f&&(p=C+H);const b=n(e);b&&p>b?(p=b,g(e,!0)):g(e,!1);const w=o.get();if(w.set&&(e.dom.setStyles(e.getDoc().documentElement,{"min-height":0}),e.dom.setStyles(e.getBody(),{"min-height":"inherit"})),p!==w.totalHeight&&(C-u!==w.contentHeight||!w.set)){const s=p-w.totalHeight;if(d.setStyle(e.getContainer(),"height",p+"px"),o.set({totalHeight:p,contentHeight:C,set:!0}),(e=>{e.dispatch("ResizeEditor")})(e),t.browser.isSafari()&&(t.os.isMacOS()||t.os.isiOS())){const t=e.getWin();t.scrollTo(t.pageXOffset,t.pageYOffset)}e.hasFocus()&&(e=>{if("setcontent"===e?.type.toLowerCase()){const t=e;return!0===t.selection||!0===t.paste}return!1})(r)&&e.selection.scrollIntoView(),(t.browser.isSafari()||t.browser.isChromium())&&s<0&&a(e,o,r,c)}};e.add("autoresize",e=>{if((e=>{const t=e.options.register;t("autoresize_overflow_padding",{processor:"number",default:1}),t("autoresize_bottom_margin",{processor:"number",default:50})})(e),e.options.isSet("resize")||e.options.set("resize",!1),!e.inline){const o=(()=>{let e={totalHeight:0,contentHeight:0,set:!1};return{get:()=>e,set:t=>{e=t}}})();((e,t)=>{e.addCommand("mceAutoResize",()=>{a(e,t)})})(e,o),((e,o)=>{const s=()=>r(e);e.on("init",n=>{const r=i(e),g=e.dom;g.setStyles(e.getDoc().documentElement,{height:"auto"}),t.browser.isEdge()||t.browser.isIE()?g.setStyles(e.getBody(),{paddingLeft:r,paddingRight:r,"min-height":0}):g.setStyles(e.getBody(),{paddingLeft:r,paddingRight:r}),a(e,o,n,s)}),e.on("NodeChange SetContent keyup FullscreenStateChanged ResizeContent",t=>{a(e,o,t,s)})})(e,o)}})}();
/**
* TinyMCE version 7.3.0 (2024-08-07)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.Env");const o=e=>t=>t.options.get(e),n=o("min_height"),s=o("max_height"),i=o("autoresize_overflow_padding"),r=o("autoresize_bottom_margin"),g=(e,t)=>{const o=e.getBody();o&&(o.style.overflowY=t?"":"hidden",t||(o.scrollTop=0))},l=(e,t,o,n)=>{var s;const i=parseInt(null!==(s=e.getStyle(t,o,n))&&void 0!==s?s:"",10);return isNaN(i)?0:i},a=(e,o,r,c)=>{var d;const u=e.dom,h=e.getDoc();if(!h)return;if((e=>e.plugins.fullscreen&&e.plugins.fullscreen.isFullscreen())(e))return void g(e,!0);const m=h.documentElement,f=c?c():i(e),p=null!==(d=n(e))&&void 0!==d?d:e.getElement().offsetHeight;let y=p;const S=l(u,m,"margin-top",!0),v=l(u,m,"margin-bottom",!0);let C=m.offsetHeight+S+v+f;C<0&&(C=0);const H=e.getContainer().offsetHeight-e.getContentAreaContainer().offsetHeight;C+H>p&&(y=C+H);const b=s(e);b&&y>b?(y=b,g(e,!0)):g(e,!1);const w=o.get();if(w.set&&(e.dom.setStyles(e.getDoc().documentElement,{"min-height":0}),e.dom.setStyles(e.getBody(),{"min-height":"inherit"})),y!==w.totalHeight&&(C-f!==w.contentHeight||!w.set)){const n=y-w.totalHeight;if(u.setStyle(e.getContainer(),"height",y+"px"),o.set({totalHeight:y,contentHeight:C,set:!0}),(e=>{e.dispatch("ResizeEditor")})(e),t.browser.isSafari()&&(t.os.isMacOS()||t.os.isiOS())){const t=e.getWin();t.scrollTo(t.pageXOffset,t.pageYOffset)}e.hasFocus()&&(e=>{if("setcontent"===(null==e?void 0:e.type.toLowerCase())){const t=e;return!0===t.selection||!0===t.paste}return!1})(r)&&e.selection.scrollIntoView(),(t.browser.isSafari()||t.browser.isChromium())&&n<0&&a(e,o,r,c)}};e.add("autoresize",(e=>{if((e=>{const t=e.options.register;t("autoresize_overflow_padding",{processor:"number",default:1}),t("autoresize_bottom_margin",{processor:"number",default:50})})(e),e.options.isSet("resize")||e.options.set("resize",!1),!e.inline){const o=(e=>{let t={totalHeight:0,contentHeight:0,set:!1};return{get:()=>t,set:e=>{t=e}}})();((e,t)=>{e.addCommand("mceAutoResize",(()=>{a(e,t)}))})(e,o),((e,o)=>{const n=()=>r(e);e.on("init",(s=>{const r=i(e),g=e.dom;g.setStyles(e.getDoc().documentElement,{height:"auto"}),t.browser.isEdge()||t.browser.isIE()?g.setStyles(e.getBody(),{paddingLeft:r,paddingRight:r,"min-height":0}):g.setStyles(e.getBody(),{paddingLeft:r,paddingRight:r}),a(e,o,s,n)})),e.on("NodeChange SetContent keyup FullscreenStateChanged ResizeContent",(t=>{a(e,o,t,n)}))})(e,o)}}))}();
-7
View File
@@ -1,7 +0,0 @@
// Exports the "autosave" plugin for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/plugins/autosave')
// ES2015:
// import 'tinymce/plugins/autosave'
require('./plugin.js');
-249
View File
@@ -1,249 +0,0 @@
/**
* TinyMCE version 8.5.1 (2026-05-19)
*/
(function () {
'use strict';
var global$4 = tinymce.util.Tools.resolve('tinymce.PluginManager');
/* eslint-disable @typescript-eslint/no-wrapper-object-types */
const hasProto = (v, constructor, predicate) => {
if (predicate(v, constructor.prototype)) {
return true;
}
else {
// String-based fallback time
return v.constructor?.name === constructor.name;
}
};
const typeOf = (x) => {
const t = typeof x;
if (x === null) {
return 'null';
}
else if (t === 'object' && Array.isArray(x)) {
return 'array';
}
else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) {
return 'string';
}
else {
return t;
}
};
const isType = (type) => (value) => typeOf(value) === type;
const eq = (t) => (a) => t === a;
const isString = isType('string');
const isUndefined = eq(undefined);
var global$3 = tinymce.util.Tools.resolve('tinymce.util.Delay');
var global$2 = tinymce.util.Tools.resolve('tinymce.util.LocalStorage');
var global$1 = tinymce.util.Tools.resolve('tinymce.util.Tools');
const fireRestoreDraft = (editor) => editor.dispatch('RestoreDraft');
const fireStoreDraft = (editor) => editor.dispatch('StoreDraft');
const fireRemoveDraft = (editor) => editor.dispatch('RemoveDraft');
const parse = (timeString) => {
const multiples = {
s: 1000,
m: 60000
};
const parsedTime = /^(\d+)([ms]?)$/.exec(timeString);
return (parsedTime && parsedTime[2] ? multiples[parsedTime[2]] : 1) * parseInt(timeString, 10);
};
const option = (name) => (editor) => editor.options.get(name);
const register$1 = (editor) => {
const registerOption = editor.options.register;
const timeProcessor = (value) => {
const valid = isString(value);
if (valid) {
return { value: parse(value), valid };
}
else {
return { valid: false, message: 'Must be a string.' };
}
};
registerOption('autosave_ask_before_unload', {
processor: 'boolean',
default: true
});
registerOption('autosave_prefix', {
processor: 'string',
default: 'tinymce-autosave-{path}{query}{hash}-{id}-'
});
registerOption('autosave_restore_when_empty', {
processor: 'boolean',
default: false
});
registerOption('autosave_interval', {
processor: timeProcessor,
default: '30s'
});
registerOption('autosave_retention', {
processor: timeProcessor,
default: '20m'
});
};
const shouldAskBeforeUnload = option('autosave_ask_before_unload');
const shouldRestoreWhenEmpty = option('autosave_restore_when_empty');
const getAutoSaveInterval = option('autosave_interval');
const getAutoSaveRetention = option('autosave_retention');
const getAutoSavePrefix = (editor) => {
const location = document.location;
return editor.options.get('autosave_prefix').replace(/{path}/g, location.pathname)
.replace(/{query}/g, location.search)
.replace(/{hash}/g, location.hash)
.replace(/{id}/g, editor.id);
};
const isEmpty = (editor, html) => {
if (isUndefined(html)) {
return editor.dom.isEmpty(editor.getBody());
}
else {
const trimmedHtml = global$1.trim(html);
if (trimmedHtml === '') {
return true;
}
else {
const fragment = new DOMParser().parseFromString(trimmedHtml, 'text/html');
return editor.dom.isEmpty(fragment);
}
}
};
const hasDraft = (editor) => {
const time = parseInt(global$2.getItem(getAutoSavePrefix(editor) + 'time') ?? '0', 10) || 0;
if (new Date().getTime() - time > getAutoSaveRetention(editor)) {
removeDraft(editor, false);
return false;
}
return true;
};
const removeDraft = (editor, fire) => {
const prefix = getAutoSavePrefix(editor);
global$2.removeItem(prefix + 'draft');
global$2.removeItem(prefix + 'time');
if (fire !== false) {
fireRemoveDraft(editor);
}
};
const storeDraft = (editor) => {
const prefix = getAutoSavePrefix(editor);
if (!isEmpty(editor) && editor.isDirty()) {
global$2.setItem(prefix + 'draft', editor.getContent({ format: 'raw', no_events: true }));
global$2.setItem(prefix + 'time', new Date().getTime().toString());
fireStoreDraft(editor);
}
};
const restoreDraft = (editor) => {
const prefix = getAutoSavePrefix(editor);
if (hasDraft(editor)) {
editor.setContent(global$2.getItem(prefix + 'draft') ?? '', { format: 'raw' });
fireRestoreDraft(editor);
}
};
const startStoreDraft = (editor) => {
const interval = getAutoSaveInterval(editor);
global$3.setEditorInterval(editor, () => {
storeDraft(editor);
}, interval);
};
const restoreLastDraft = (editor) => {
editor.undoManager.transact(() => {
restoreDraft(editor);
removeDraft(editor);
});
editor.focus();
};
const get = (editor) => ({
hasDraft: () => hasDraft(editor),
storeDraft: () => storeDraft(editor),
restoreDraft: () => restoreDraft(editor),
removeDraft: (fire) => removeDraft(editor, fire),
isEmpty: (html) => isEmpty(editor, html)
});
var global = tinymce.util.Tools.resolve('tinymce.EditorManager');
const setup = (editor) => {
editor.editorManager.on('BeforeUnload', (e) => {
let msg;
global$1.each(global.get(), (editor) => {
// Store a draft for each editor instance
if (editor.plugins.autosave) {
editor.plugins.autosave.storeDraft();
}
// Setup a return message if the editor is dirty
if (!msg && editor.isDirty() && shouldAskBeforeUnload(editor)) {
msg = editor.translate('You have unsaved changes are you sure you want to navigate away?');
}
});
if (msg) {
e.preventDefault();
e.returnValue = msg;
}
});
};
const makeSetupHandler = (editor) => (api) => {
const shouldEnable = () => hasDraft(editor) && !editor.mode.isReadOnly();
api.setEnabled(shouldEnable());
const editorEventCallback = () => api.setEnabled(shouldEnable());
editor.on('StoreDraft RestoreDraft RemoveDraft', editorEventCallback);
return () => editor.off('StoreDraft RestoreDraft RemoveDraft', editorEventCallback);
};
const register = (editor) => {
// TODO: This was moved from makeSetupHandler as it would only be called when the menu item was rendered?
// Is it safe to start this process when the plugin is registered?
startStoreDraft(editor);
const onAction = () => {
restoreLastDraft(editor);
};
editor.ui.registry.addButton('restoredraft', {
tooltip: 'Restore last draft',
icon: 'restore-draft',
onAction,
onSetup: makeSetupHandler(editor)
});
editor.ui.registry.addMenuItem('restoredraft', {
text: 'Restore last draft',
icon: 'restore-draft',
onAction,
onSetup: makeSetupHandler(editor)
});
};
/**
* This class contains all core logic for the autosave plugin.
*
* @class tinymce.autosave.Plugin
* @private
*/
var Plugin = () => {
global$4.add('autosave', (editor) => {
register$1(editor);
setup(editor);
register(editor);
editor.on('init', () => {
if (shouldRestoreWhenEmpty(editor) && editor.dom.isEmpty(editor.getBody())) {
restoreDraft(editor);
}
});
return get(editor);
});
};
Plugin();
/** *****
* DO NOT EXPORT ANYTHING
*
* IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE
*******/
})();
+4 -1
View File
@@ -1 +1,4 @@
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=t=>"string"===(t=>{const e=typeof t;return null===t?"null":"object"===e&&Array.isArray(t)?"array":"object"===e&&(r=o=t,(a=String).prototype.isPrototypeOf(r)||o.constructor?.name===a.name)?"string":e;var r,o,a})(t);const r=t=>undefined===t;var o=tinymce.util.Tools.resolve("tinymce.util.Delay"),a=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),s=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=t=>{const e=/^(\d+)([ms]?)$/.exec(t);return(e&&e[2]?{s:1e3,m:6e4}[e[2]]:1)*parseInt(t,10)},i=t=>e=>e.options.get(t),u=i("autosave_ask_before_unload"),c=i("autosave_restore_when_empty"),l=i("autosave_interval"),m=i("autosave_retention"),d=t=>{const e=document.location;return t.options.get("autosave_prefix").replace(/{path}/g,e.pathname).replace(/{query}/g,e.search).replace(/{hash}/g,e.hash).replace(/{id}/g,t.id)},f=(t,e)=>{if(r(e))return t.dom.isEmpty(t.getBody());{const r=s.trim(e);if(""===r)return!0;{const e=(new DOMParser).parseFromString(r,"text/html");return t.dom.isEmpty(e)}}},v=t=>{const e=parseInt(a.getItem(d(t)+"time")??"0",10)||0;return!((new Date).getTime()-e>m(t)&&(p(t,!1),1))},p=(t,e)=>{const r=d(t);a.removeItem(r+"draft"),a.removeItem(r+"time"),!1!==e&&(t=>{t.dispatch("RemoveDraft")})(t)},y=t=>{const e=d(t);!f(t)&&t.isDirty()&&(a.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),a.setItem(e+"time",(new Date).getTime().toString()),(t=>{t.dispatch("StoreDraft")})(t))},g=t=>{const e=d(t);v(t)&&(t.setContent(a.getItem(e+"draft")??"",{format:"raw"}),(t=>{t.dispatch("RestoreDraft")})(t))};var D=tinymce.util.Tools.resolve("tinymce.EditorManager");const h=t=>e=>{const r=()=>v(t)&&!t.mode.isReadOnly();e.setEnabled(r());const o=()=>e.setEnabled(r());return t.on("StoreDraft RestoreDraft RemoveDraft",o),()=>t.off("StoreDraft RestoreDraft RemoveDraft",o)};t.add("autosave",t=>((t=>{const r=t.options.register,o=t=>{const r=e(t);return r?{value:n(t),valid:r}:{valid:!1,message:"Must be a string."}};r("autosave_ask_before_unload",{processor:"boolean",default:!0}),r("autosave_prefix",{processor:"string",default:"tinymce-autosave-{path}{query}{hash}-{id}-"}),r("autosave_restore_when_empty",{processor:"boolean",default:!1}),r("autosave_interval",{processor:o,default:"30s"}),r("autosave_retention",{processor:o,default:"20m"})})(t),(t=>{t.editorManager.on("BeforeUnload",t=>{let e;s.each(D.get(),t=>{t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&u(t)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e&&(t.preventDefault(),t.returnValue=e)})})(t),(t=>{(t=>{const e=l(t);o.setEditorInterval(t,()=>{y(t)},e)})(t);const e=()=>{(t=>{t.undoManager.transact(()=>{g(t),p(t)}),t.focus()})(t)};t.ui.registry.addButton("restoredraft",{tooltip:"Restore last draft",icon:"restore-draft",onAction:e,onSetup:h(t)}),t.ui.registry.addMenuItem("restoredraft",{text:"Restore last draft",icon:"restore-draft",onAction:e,onSetup:h(t)})})(t),t.on("init",()=>{c(t)&&t.dom.isEmpty(t.getBody())&&g(t)}),(t=>({hasDraft:()=>v(t),storeDraft:()=>y(t),restoreDraft:()=>g(t),removeDraft:e=>p(t,e),isEmpty:e=>f(t,e)}))(t)))}();
/**
* TinyMCE version 7.3.0 (2024-08-07)
*/
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=("string",t=>"string"===(t=>{const e=typeof t;return null===t?"null":"object"===e&&Array.isArray(t)?"array":"object"===e&&(r=o=t,(a=String).prototype.isPrototypeOf(r)||(null===(s=o.constructor)||void 0===s?void 0:s.name)===a.name)?"string":e;var r,o,a,s})(t));const r=(void 0,t=>undefined===t);var o=tinymce.util.Tools.resolve("tinymce.util.Delay"),a=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),s=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=t=>{const e=/^(\d+)([ms]?)$/.exec(t);return(e&&e[2]?{s:1e3,m:6e4}[e[2]]:1)*parseInt(t,10)},i=t=>e=>e.options.get(t),u=i("autosave_ask_before_unload"),l=i("autosave_restore_when_empty"),c=i("autosave_interval"),d=i("autosave_retention"),m=t=>{const e=document.location;return t.options.get("autosave_prefix").replace(/{path}/g,e.pathname).replace(/{query}/g,e.search).replace(/{hash}/g,e.hash).replace(/{id}/g,t.id)},v=(t,e)=>{if(r(e))return t.dom.isEmpty(t.getBody());{const r=s.trim(e);if(""===r)return!0;{const e=(new DOMParser).parseFromString(r,"text/html");return t.dom.isEmpty(e)}}},f=t=>{var e;const r=parseInt(null!==(e=a.getItem(m(t)+"time"))&&void 0!==e?e:"0",10)||0;return!((new Date).getTime()-r>d(t)&&(p(t,!1),1))},p=(t,e)=>{const r=m(t);a.removeItem(r+"draft"),a.removeItem(r+"time"),!1!==e&&(t=>{t.dispatch("RemoveDraft")})(t)},g=t=>{const e=m(t);!v(t)&&t.isDirty()&&(a.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),a.setItem(e+"time",(new Date).getTime().toString()),(t=>{t.dispatch("StoreDraft")})(t))},y=t=>{var e;const r=m(t);f(t)&&(t.setContent(null!==(e=a.getItem(r+"draft"))&&void 0!==e?e:"",{format:"raw"}),(t=>{t.dispatch("RestoreDraft")})(t))};var D=tinymce.util.Tools.resolve("tinymce.EditorManager");const h=t=>e=>{e.setEnabled(f(t));const r=()=>e.setEnabled(f(t));return t.on("StoreDraft RestoreDraft RemoveDraft",r),()=>t.off("StoreDraft RestoreDraft RemoveDraft",r)};t.add("autosave",(t=>((t=>{const r=t.options.register,o=t=>{const r=e(t);return r?{value:n(t),valid:r}:{valid:!1,message:"Must be a string."}};r("autosave_ask_before_unload",{processor:"boolean",default:!0}),r("autosave_prefix",{processor:"string",default:"tinymce-autosave-{path}{query}{hash}-{id}-"}),r("autosave_restore_when_empty",{processor:"boolean",default:!1}),r("autosave_interval",{processor:o,default:"30s"}),r("autosave_retention",{processor:o,default:"20m"})})(t),(t=>{t.editorManager.on("BeforeUnload",(t=>{let e;s.each(D.get(),(t=>{t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&u(t)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))})),e&&(t.preventDefault(),t.returnValue=e)}))})(t),(t=>{(t=>{const e=c(t);o.setEditorInterval(t,(()=>{g(t)}),e)})(t);const e=()=>{(t=>{t.undoManager.transact((()=>{y(t),p(t)})),t.focus()})(t)};t.ui.registry.addButton("restoredraft",{tooltip:"Restore last draft",icon:"restore-draft",onAction:e,onSetup:h(t)}),t.ui.registry.addMenuItem("restoredraft",{text:"Restore last draft",icon:"restore-draft",onAction:e,onSetup:h(t)})})(t),t.on("init",(()=>{l(t)&&t.dom.isEmpty(t.getBody())&&y(t)})),(t=>({hasDraft:()=>f(t),storeDraft:()=>g(t),restoreDraft:()=>y(t),removeDraft:e=>p(t,e),isEmpty:e=>v(t,e)}))(t))))}();
-7
View File
@@ -1,7 +0,0 @@
// Exports the "charmap" plugin for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/plugins/charmap')
// ES2015:
// import 'tinymce/plugins/charmap'
require('./plugin.js');
-997
View File
@@ -1,997 +0,0 @@
/**
* TinyMCE version 8.5.1 (2026-05-19)
*/
(function () {
'use strict';
var global$1 = tinymce.util.Tools.resolve('tinymce.PluginManager');
const fireInsertCustomChar = (editor, chr) => {
return editor.dispatch('insertCustomChar', { chr });
};
const insertChar = (editor, chr) => {
const evtChr = fireInsertCustomChar(editor, chr).chr;
editor.execCommand('mceInsertContent', false, evtChr);
};
/* eslint-disable @typescript-eslint/no-wrapper-object-types */
const hasProto = (v, constructor, predicate) => {
if (predicate(v, constructor.prototype)) {
return true;
}
else {
// String-based fallback time
return v.constructor?.name === constructor.name;
}
};
const typeOf = (x) => {
const t = typeof x;
if (x === null) {
return 'null';
}
else if (t === 'object' && Array.isArray(x)) {
return 'array';
}
else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) {
return 'string';
}
else {
return t;
}
};
const isType = (type) => (value) => typeOf(value) === type;
const isSimpleType = (type) => (value) => typeof value === type;
const eq = (t) => (a) => t === a;
const isArray$1 = isType('array');
const isNull = eq(null);
const isUndefined = eq(undefined);
const isNullable = (a) => a === null || a === undefined;
const isNonNullable = (a) => !isNullable(a);
const isFunction = isSimpleType('function');
const constant = (value) => {
return () => {
return value;
};
};
const never = constant(false);
/**
* The `Optional` type represents a value (of any type) that potentially does
* not exist. Any `Optional<T>` can either be a `Some<T>` (in which case the
* value does exist) or a `None` (in which case the value does not exist). This
* module defines a whole lot of FP-inspired utility functions for dealing with
* `Optional` objects.
*
* Comparison with null or undefined:
* - We don't get fancy null coalescing operators with `Optional`
* - We do get fancy helper functions with `Optional`
* - `Optional` support nesting, and allow for the type to still be nullable (or
* another `Optional`)
* - There is no option to turn off strict-optional-checks like there is for
* strict-null-checks
*/
class Optional {
tag;
value;
// Sneaky optimisation: every instance of Optional.none is identical, so just
// reuse the same object
static singletonNone = new Optional(false);
// The internal representation has a `tag` and a `value`, but both are
// private: able to be console.logged, but not able to be accessed by code
constructor(tag, value) {
this.tag = tag;
this.value = value;
}
// --- Identities ---
/**
* Creates a new `Optional<T>` that **does** contain a value.
*/
static some(value) {
return new Optional(true, value);
}
/**
* Create a new `Optional<T>` that **does not** contain a value. `T` can be
* any type because we don't actually have a `T`.
*/
static none() {
return Optional.singletonNone;
}
/**
* Perform a transform on an `Optional` type. Regardless of whether this
* `Optional` contains a value or not, `fold` will return a value of type `U`.
* If this `Optional` does not contain a value, the `U` will be created by
* calling `onNone`. If this `Optional` does contain a value, the `U` will be
* created by calling `onSome`.
*
* For the FP enthusiasts in the room, this function:
* 1. Could be used to implement all of the functions below
* 2. Forms a catamorphism
*/
fold(onNone, onSome) {
if (this.tag) {
return onSome(this.value);
}
else {
return onNone();
}
}
/**
* Determine if this `Optional` object contains a value.
*/
isSome() {
return this.tag;
}
/**
* Determine if this `Optional` object **does not** contain a value.
*/
isNone() {
return !this.tag;
}
// --- Functor (name stolen from Haskell / maths) ---
/**
* Perform a transform on an `Optional` object, **if** there is a value. If
* you provide a function to turn a T into a U, this is the function you use
* to turn an `Optional<T>` into an `Optional<U>`. If this **does** contain
* a value then the output will also contain a value (that value being the
* output of `mapper(this.value)`), and if this **does not** contain a value
* then neither will the output.
*/
map(mapper) {
if (this.tag) {
return Optional.some(mapper(this.value));
}
else {
return Optional.none();
}
}
// --- Monad (name stolen from Haskell / maths) ---
/**
* Perform a transform on an `Optional` object, **if** there is a value.
* Unlike `map`, here the transform itself also returns an `Optional`.
*/
bind(binder) {
if (this.tag) {
return binder(this.value);
}
else {
return Optional.none();
}
}
// --- Traversable (name stolen from Haskell / maths) ---
/**
* For a given predicate, this function finds out if there **exists** a value
* inside this `Optional` object that meets the predicate. In practice, this
* means that for `Optional`s that do not contain a value it returns false (as
* no predicate-meeting value exists).
*/
exists(predicate) {
return this.tag && predicate(this.value);
}
/**
* For a given predicate, this function finds out if **all** the values inside
* this `Optional` object meet the predicate. In practice, this means that
* for `Optional`s that do not contain a value it returns true (as all 0
* objects do meet the predicate).
*/
forall(predicate) {
return !this.tag || predicate(this.value);
}
filter(predicate) {
if (!this.tag || predicate(this.value)) {
return this;
}
else {
return Optional.none();
}
}
// --- Getters ---
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided `Optional` object does not contain a
* value.
*/
getOr(replacement) {
return this.tag ? this.value : replacement;
}
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided `Optional` object does not contain a
* value. Unlike `getOr`, in this method the `replacement` object is also
* `Optional` - meaning that this method will always return an `Optional`.
*/
or(replacement) {
return this.tag ? this : replacement;
}
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided `Optional` object does not contain a
* value. Unlike `getOr`, in this method the `replacement` value is
* "thunked" - that is to say that you don't pass a value to `getOrThunk`, you
* pass a function which (if called) will **return** the `value` you want to
* use.
*/
getOrThunk(thunk) {
return this.tag ? this.value : thunk();
}
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided Optional object does not contain a
* value.
*
* Unlike `or`, in this method the `replacement` value is "thunked" - that is
* to say that you don't pass a value to `orThunk`, you pass a function which
* (if called) will **return** the `value` you want to use.
*
* Unlike `getOrThunk`, in this method the `replacement` value is also
* `Optional`, meaning that this method will always return an `Optional`.
*/
orThunk(thunk) {
return this.tag ? this : thunk();
}
/**
* Get the value out of the inside of the `Optional` object, throwing an
* exception if the provided `Optional` object does not contain a value.
*
* WARNING:
* You should only be using this function if you know that the `Optional`
* object **is not** empty (otherwise you're throwing exceptions in production
* code, which is bad).
*
* In tests this is more acceptable.
*
* Prefer other methods to this, such as `.each`.
*/
getOrDie(message) {
if (!this.tag) {
throw new Error(message ?? 'Called getOrDie on None');
}
else {
return this.value;
}
}
// --- Interop with null and undefined ---
/**
* Creates an `Optional` value from a nullable (or undefined-able) input.
* Null, or undefined, is converted to `None`, and anything else is converted
* to `Some`.
*/
static from(value) {
return isNonNullable(value) ? Optional.some(value) : Optional.none();
}
/**
* Converts an `Optional` to a nullable type, by getting the value if it
* exists, or returning `null` if it does not.
*/
getOrNull() {
return this.tag ? this.value : null;
}
/**
* Converts an `Optional` to an undefined-able type, by getting the value if
* it exists, or returning `undefined` if it does not.
*/
getOrUndefined() {
return this.value;
}
// --- Utilities ---
/**
* If the `Optional` contains a value, perform an action on that value.
* Unlike the rest of the methods on this type, `.each` has side-effects. If
* you want to transform an `Optional<T>` **into** something, then this is not
* the method for you. If you want to use an `Optional<T>` to **do**
* something, then this is the method for you - provided you're okay with not
* doing anything in the case where the `Optional` doesn't have a value inside
* it. If you're not sure whether your use-case fits into transforming
* **into** something or **doing** something, check whether it has a return
* value. If it does, you should be performing a transform.
*/
each(worker) {
if (this.tag) {
worker(this.value);
}
}
/**
* Turn the `Optional` object into an array that contains all of the values
* stored inside the `Optional`. In practice, this means the output will have
* either 0 or 1 elements.
*/
toArray() {
return this.tag ? [this.value] : [];
}
/**
* Turn the `Optional` object into a string for debugging or printing. Not
* recommended for production code, but good for debugging. Also note that
* these days an `Optional` object can be logged to the console directly, and
* its inner value (if it exists) will be visible.
*/
toString() {
return this.tag ? `some(${this.value})` : 'none()';
}
}
const nativeSlice = Array.prototype.slice;
const nativePush = Array.prototype.push;
const map = (xs, f) => {
// pre-allocating array size when it's guaranteed to be known
// http://jsperf.com/push-allocated-vs-dynamic/22
const len = xs.length;
const r = new Array(len);
for (let i = 0; i < len; i++) {
const x = xs[i];
r[i] = f(x, i);
}
return r;
};
// Unwound implementing other functions in terms of each.
// The code size is roughly the same, and it should allow for better optimisation.
// const each = function<T, U>(xs: T[], f: (x: T, i?: number, xs?: T[]) => void): void {
const each = (xs, f) => {
for (let i = 0, len = xs.length; i < len; i++) {
const x = xs[i];
f(x, i);
}
};
const findUntil = (xs, pred, until) => {
for (let i = 0, len = xs.length; i < len; i++) {
const x = xs[i];
if (pred(x, i)) {
return Optional.some(x);
}
else if (until(x, i)) {
break;
}
}
return Optional.none();
};
const find = (xs, pred) => {
return findUntil(xs, pred, never);
};
const flatten = (xs) => {
// Note, this is possible because push supports multiple arguments:
// http://jsperf.com/concat-push/6
// Note that in the past, concat() would silently work (very slowly) for array-like objects.
// With this change it will throw an error.
const r = [];
for (let i = 0, len = xs.length; i < len; ++i) {
// Ensure that each value is an array itself
if (!isArray$1(xs[i])) {
throw new Error('Arr.flatten item ' + i + ' was not an array, input: ' + xs);
}
nativePush.apply(r, xs[i]);
}
return r;
};
const bind = (xs, f) => flatten(map(xs, f));
isFunction(Array.from) ? Array.from : (x) => nativeSlice.call(x);
const Cell = (initial) => {
let value = initial;
const get = () => {
return value;
};
const set = (v) => {
value = v;
};
return {
get,
set
};
};
const contains = (str, substr, start = 0, end) => {
const idx = str.indexOf(substr, start);
if (idx !== -1) {
return isUndefined(end) ? true : idx + substr.length <= end;
}
else {
return false;
}
};
const fromCodePoint = String.fromCodePoint;
// Run a function fn after rate ms. If another invocation occurs
// during the time it is waiting, reschedule the function again
// with the new arguments.
const last = (fn, rate) => {
let timer = null;
const cancel = () => {
if (!isNull(timer)) {
clearTimeout(timer);
timer = null;
}
};
const throttle = (...args) => {
cancel();
timer = setTimeout(() => {
timer = null;
fn.apply(null, args);
}, rate);
};
return {
cancel,
throttle
};
};
var global = tinymce.util.Tools.resolve('tinymce.util.Tools');
const option = (name) => (editor) => editor.options.get(name);
const register$2 = (editor) => {
const registerOption = editor.options.register;
const charMapProcessor = (value) => isFunction(value) || isArray$1(value);
registerOption('charmap', {
processor: charMapProcessor,
});
registerOption('charmap_append', {
processor: charMapProcessor
});
};
const getCharMap$1 = option('charmap');
const getCharMapAppend = option('charmap_append');
const isArray = global.isArray;
const UserDefined = 'User Defined';
const getDefaultCharMap = () => {
return [
// TODO: Merge categories with TBIO
// {
// name: 'Unknown',
// characters : [
// [160, 'no-break space'],
// [173, 'soft hyphen'],
// [34, 'quotation mark']
// ]
// },
{ name: 'Currency',
characters: [
[36, 'dollar sign'],
[162, 'cent sign'],
[8364, 'euro sign'],
[163, 'pound sign'],
[165, 'yen sign'],
[164, 'currency sign'],
[8352, 'euro-currency sign'],
[8353, 'colon sign'],
[8354, 'cruzeiro sign'],
[8355, 'french franc sign'],
[8356, 'lira sign'],
[8357, 'mill sign'],
[8358, 'naira sign'],
[8359, 'peseta sign'],
[8360, 'rupee sign'],
[8361, 'won sign'],
[8362, 'new sheqel sign'],
[8363, 'dong sign'],
[8365, 'kip sign'],
[8366, 'tugrik sign'],
[8367, 'drachma sign'],
[8368, 'german penny symbol'],
[8369, 'peso sign'],
[8370, 'guarani sign'],
[8371, 'austral sign'],
[8372, 'hryvnia sign'],
[8373, 'cedi sign'],
[8374, 'livre tournois sign'],
[8375, 'spesmilo sign'],
[8376, 'tenge sign'],
[8377, 'indian rupee sign'],
[8378, 'turkish lira sign'],
[8379, 'nordic mark sign'],
[8380, 'manat sign'],
[8381, 'ruble sign'],
[20870, 'yen character'],
[20803, 'yuan character'],
[22291, 'yuan character, in hong kong and taiwan'],
[22278, 'yen/yuan character variant one']
]
},
{ name: 'Text',
characters: [
[169, 'copyright sign'],
[174, 'registered sign'],
[8482, 'trade mark sign'],
[8240, 'per mille sign'],
[181, 'micro sign'],
[183, 'middle dot'],
[8226, 'bullet'],
[8230, 'three dot leader'],
[8242, 'minutes / feet'],
[8243, 'seconds / inches'],
[167, 'section sign'],
[182, 'paragraph sign'],
[223, 'sharp s / ess-zed']
]
},
{ name: 'Quotations',
characters: [
[8249, 'single left-pointing angle quotation mark'],
[8250, 'single right-pointing angle quotation mark'],
[171, 'left pointing guillemet'],
[187, 'right pointing guillemet'],
[8216, 'left single quotation mark'],
[8217, 'right single quotation mark'],
[8220, 'left double quotation mark'],
[8221, 'right double quotation mark'],
[8218, 'single low-9 quotation mark'],
[8222, 'double low-9 quotation mark'],
[60, 'less-than sign'],
[62, 'greater-than sign'],
[8804, 'less-than or equal to'],
[8805, 'greater-than or equal to'],
[8211, 'en dash'],
[8212, 'em dash'],
[175, 'macron'],
[8254, 'overline'],
[164, 'currency sign'],
[166, 'broken bar'],
[168, 'diaeresis'],
[161, 'inverted exclamation mark'],
[191, 'turned question mark'],
[710, 'circumflex accent'],
[732, 'small tilde'],
[176, 'degree sign'],
[8722, 'minus sign'],
[177, 'plus-minus sign'],
[247, 'division sign'],
[8260, 'fraction slash'],
[215, 'multiplication sign'],
[185, 'superscript one'],
[178, 'superscript two'],
[179, 'superscript three'],
[188, 'fraction one quarter'],
[189, 'fraction one half'],
[190, 'fraction three quarters']
]
},
{
name: 'Mathematical',
characters: [
[402, 'function / florin'],
[8747, 'integral'],
[8721, 'n-ary sumation'],
[8734, 'infinity'],
[8730, 'square root'],
[8764, 'similar to'],
[8773, 'approximately equal to'],
[8776, 'almost equal to'],
[8800, 'not equal to'],
[8801, 'identical to'],
[8712, 'element of'],
[8713, 'not an element of'],
[8715, 'contains as member'],
[8719, 'n-ary product'],
[8743, 'logical and'],
[8744, 'logical or'],
[172, 'not sign'],
[8745, 'intersection'],
[8746, 'union'],
[8706, 'partial differential'],
[8704, 'for all'],
[8707, 'there exists'],
[8709, 'diameter'],
[8711, 'backward difference'],
[8727, 'asterisk operator'],
[8733, 'proportional to'],
[8736, 'angle']
]
},
// TODO: Merge categories with TBIO
// {
// name: 'Undefined',
// characters: [
// [180, 'acute accent'],
// [184, 'cedilla'],
// [170, 'feminine ordinal indicator'],
// [186, 'masculine ordinal indicator'],
// [8224, 'dagger'],
// [8225, 'double dagger']
// ]
// },
{
name: 'Extended Latin',
characters: [
[192, 'A - grave'],
[193, 'A - acute'],
[194, 'A - circumflex'],
[195, 'A - tilde'],
[196, 'A - diaeresis'],
[197, 'A - ring above'],
[256, 'A - macron'],
[198, 'ligature AE'],
[199, 'C - cedilla'],
[200, 'E - grave'],
[201, 'E - acute'],
[202, 'E - circumflex'],
[203, 'E - diaeresis'],
[274, 'E - macron'],
[204, 'I - grave'],
[205, 'I - acute'],
[206, 'I - circumflex'],
[207, 'I - diaeresis'],
[298, 'I - macron'],
[208, 'ETH'],
[209, 'N - tilde'],
[210, 'O - grave'],
[211, 'O - acute'],
[212, 'O - circumflex'],
[213, 'O - tilde'],
[214, 'O - diaeresis'],
[216, 'O - slash'],
[332, 'O - macron'],
[338, 'ligature OE'],
[352, 'S - caron'],
[217, 'U - grave'],
[218, 'U - acute'],
[219, 'U - circumflex'],
[220, 'U - diaeresis'],
[362, 'U - macron'],
[221, 'Y - acute'],
[376, 'Y - diaeresis'],
[562, 'Y - macron'],
[222, 'THORN'],
[224, 'a - grave'],
[225, 'a - acute'],
[226, 'a - circumflex'],
[227, 'a - tilde'],
[228, 'a - diaeresis'],
[229, 'a - ring above'],
[257, 'a - macron'],
[230, 'ligature ae'],
[231, 'c - cedilla'],
[232, 'e - grave'],
[233, 'e - acute'],
[234, 'e - circumflex'],
[235, 'e - diaeresis'],
[275, 'e - macron'],
[236, 'i - grave'],
[237, 'i - acute'],
[238, 'i - circumflex'],
[239, 'i - diaeresis'],
[299, 'i - macron'],
[240, 'eth'],
[241, 'n - tilde'],
[242, 'o - grave'],
[243, 'o - acute'],
[244, 'o - circumflex'],
[245, 'o - tilde'],
[246, 'o - diaeresis'],
[248, 'o slash'],
[333, 'o macron'],
[339, 'ligature oe'],
[353, 's - caron'],
[249, 'u - grave'],
[250, 'u - acute'],
[251, 'u - circumflex'],
[252, 'u - diaeresis'],
[363, 'u - macron'],
[253, 'y - acute'],
[254, 'thorn'],
[255, 'y - diaeresis'],
[563, 'y - macron'],
[913, 'Alpha'],
[914, 'Beta'],
[915, 'Gamma'],
[916, 'Delta'],
[917, 'Epsilon'],
[918, 'Zeta'],
[919, 'Eta'],
[920, 'Theta'],
[921, 'Iota'],
[922, 'Kappa'],
[923, 'Lambda'],
[924, 'Mu'],
[925, 'Nu'],
[926, 'Xi'],
[927, 'Omicron'],
[928, 'Pi'],
[929, 'Rho'],
[931, 'Sigma'],
[932, 'Tau'],
[933, 'Upsilon'],
[934, 'Phi'],
[935, 'Chi'],
[936, 'Psi'],
[937, 'Omega'],
[945, 'alpha'],
[946, 'beta'],
[947, 'gamma'],
[948, 'delta'],
[949, 'epsilon'],
[950, 'zeta'],
[951, 'eta'],
[952, 'theta'],
[953, 'iota'],
[954, 'kappa'],
[955, 'lambda'],
[956, 'mu'],
[957, 'nu'],
[958, 'xi'],
[959, 'omicron'],
[960, 'pi'],
[961, 'rho'],
[962, 'final sigma'],
[963, 'sigma'],
[964, 'tau'],
[965, 'upsilon'],
[966, 'phi'],
[967, 'chi'],
[968, 'psi'],
[969, 'omega']
]
},
{
name: 'Symbols',
characters: [
[8501, 'alef symbol'],
[982, 'pi symbol'],
[8476, 'real part symbol'],
[978, 'upsilon - hook symbol'],
[8472, 'Weierstrass p'],
[8465, 'imaginary part']
]
},
{
name: 'Arrows',
characters: [
[8592, 'leftwards arrow'],
[8593, 'upwards arrow'],
[8594, 'rightwards arrow'],
[8595, 'downwards arrow'],
[8596, 'left right arrow'],
[8629, 'carriage return'],
[8656, 'leftwards double arrow'],
[8657, 'upwards double arrow'],
[8658, 'rightwards double arrow'],
[8659, 'downwards double arrow'],
[8660, 'left right double arrow'],
[8756, 'therefore'],
[8834, 'subset of'],
[8835, 'superset of'],
[8836, 'not a subset of'],
[8838, 'subset of or equal to'],
[8839, 'superset of or equal to'],
[8853, 'circled plus'],
[8855, 'circled times'],
[8869, 'perpendicular'],
[8901, 'dot operator'],
[8968, 'left ceiling'],
[8969, 'right ceiling'],
[8970, 'left floor'],
[8971, 'right floor'],
[9001, 'left-pointing angle bracket'],
[9002, 'right-pointing angle bracket'],
[9674, 'lozenge'],
[9824, 'black spade suit'],
[9827, 'black club suit'],
[9829, 'black heart suit'],
[9830, 'black diamond suit'],
[8194, 'en space'],
[8195, 'em space'],
[8201, 'thin space'],
[8204, 'zero width non-joiner'],
[8205, 'zero width joiner'],
[8206, 'left-to-right mark'],
[8207, 'right-to-left mark']
]
}
];
};
const charmapFilter = (charmap) => {
return global.grep(charmap, (item) => {
return isArray(item) && item.length === 2;
});
};
const getCharsFromOption = (optionValue) => {
if (isArray(optionValue)) {
return charmapFilter(optionValue);
}
if (typeof optionValue === 'function') {
return optionValue();
}
return [];
};
const extendCharMap = (editor, charmap) => {
const userCharMap = getCharMap$1(editor);
if (userCharMap) {
charmap = [{ name: UserDefined, characters: getCharsFromOption(userCharMap) }];
}
const userCharMapAppend = getCharMapAppend(editor);
if (userCharMapAppend) {
const userDefinedGroup = global.grep(charmap, (cg) => cg.name === UserDefined);
if (userDefinedGroup.length) {
userDefinedGroup[0].characters = [...userDefinedGroup[0].characters, ...getCharsFromOption(userCharMapAppend)];
return charmap;
}
return charmap.concat({ name: UserDefined, characters: getCharsFromOption(userCharMapAppend) });
}
return charmap;
};
const getCharMap = (editor) => {
const groups = extendCharMap(editor, getDefaultCharMap());
return groups.length > 1 ? [
{
name: 'All',
characters: bind(groups, (g) => g.characters)
}
].concat(groups) : groups;
};
const get = (editor) => {
const getCharMap$1 = () => {
return getCharMap(editor);
};
const insertChar$1 = (chr) => {
insertChar(editor, chr);
};
return {
getCharMap: getCharMap$1,
insertChar: insertChar$1
};
};
const charMatches = (charCode, name, lowerCasePattern) => {
if (contains(fromCodePoint(charCode).toLowerCase(), lowerCasePattern)) {
return true;
}
else {
return contains(name.toLowerCase(), lowerCasePattern) || contains(name.toLowerCase().replace(/\s+/g, ''), lowerCasePattern);
}
};
const scan = (group, pattern) => {
const matches = [];
const lowerCasePattern = pattern.toLowerCase();
each(group.characters, (g) => {
if (charMatches(g[0], g[1], lowerCasePattern)) {
matches.push(g);
}
});
return map(matches, (m) => ({
text: m[1],
value: fromCodePoint(m[0]),
icon: fromCodePoint(m[0])
}));
};
const patternName = 'pattern';
const open = (editor, charMap) => {
const makeGroupItems = () => [
{
label: 'Search',
type: 'input',
name: patternName
},
{
type: 'collection',
name: 'results'
// TODO TINY-3229 implement collection columns properly
// columns: 'auto'
}
];
const makeTabs = () => map(charMap, (charGroup) => ({
title: charGroup.name,
name: charGroup.name,
items: makeGroupItems()
}));
const makePanel = () => ({ type: 'panel', items: makeGroupItems() });
const makeTabPanel = () => ({ type: 'tabpanel', tabs: makeTabs() });
const currentTab = charMap.length === 1 ? Cell(UserDefined) : Cell('All');
const scanAndSet = (dialogApi, pattern) => {
find(charMap, (group) => group.name === currentTab.get()).each((f) => {
const items = scan(f, pattern);
dialogApi.setData({
results: items
});
});
};
const SEARCH_DELAY = 40;
const updateFilter = last((dialogApi) => {
const pattern = dialogApi.getData().pattern;
scanAndSet(dialogApi, pattern);
}, SEARCH_DELAY);
const body = charMap.length === 1 ? makePanel() : makeTabPanel();
const initialData = {
pattern: '',
results: scan(charMap[0], '')
};
const bridgeSpec = {
title: 'Special Character',
size: 'normal',
body,
buttons: [
{
type: 'cancel',
name: 'close',
text: 'Close',
primary: true
}
],
initialData,
onAction: (api, details) => {
if (details.name === 'results') {
insertChar(editor, details.value);
api.close();
}
},
onTabChange: (dialogApi, details) => {
currentTab.set(details.newTabName);
updateFilter.throttle(dialogApi);
},
onChange: (dialogApi, changeData) => {
if (changeData.name === patternName) {
updateFilter.throttle(dialogApi);
}
}
};
const dialogApi = editor.windowManager.open(bridgeSpec);
dialogApi.focus(patternName);
};
const register$1 = (editor, charMap) => {
editor.addCommand('mceShowCharmap', () => {
open(editor, charMap);
});
};
const init = (editor, all) => {
editor.ui.registry.addAutocompleter('charmap', {
trigger: ':',
columns: 'auto',
minChars: 2,
fetch: (pattern, _maxResults) => new Promise((resolve, _reject) => {
resolve(scan(all, pattern));
}),
onAction: (autocompleteApi, rng, value) => {
editor.selection.setRng(rng);
editor.insertContent(value);
autocompleteApi.hide();
}
});
};
const onSetupEditable = (editor) => (api) => {
const nodeChanged = () => {
api.setEnabled(editor.selection.isEditable());
};
editor.on('NodeChange', nodeChanged);
nodeChanged();
return () => {
editor.off('NodeChange', nodeChanged);
};
};
const register = (editor) => {
const onAction = () => editor.execCommand('mceShowCharmap');
editor.ui.registry.addButton('charmap', {
icon: 'insert-character',
tooltip: 'Special character',
onAction,
onSetup: onSetupEditable(editor)
});
editor.ui.registry.addMenuItem('charmap', {
icon: 'insert-character',
text: 'Special character...',
onAction,
onSetup: onSetupEditable(editor)
});
};
var Plugin = () => {
global$1.add('charmap', (editor) => {
register$2(editor);
const charMap = getCharMap(editor);
register$1(editor, charMap);
register(editor);
init(editor, charMap[0]);
return get(editor);
});
};
Plugin();
/** *****
* DO NOT EXPORT ANYTHING
*
* IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE
*******/
})();
File diff suppressed because one or more lines are too long
-7
View File
@@ -1,7 +0,0 @@
// Exports the "code" plugin for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/plugins/code')
// ES2015:
// import 'tinymce/plugins/code'
require('./plugin.js');
-98
View File
@@ -1,98 +0,0 @@
/**
* TinyMCE version 8.5.1 (2026-05-19)
*/
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
const setContent = (editor, html) => {
// We get a lovely "Wrong document" error in IE 11 if we
// don't move the focus to the editor before creating an undo
// transaction since it tries to make a bookmark for the current selection
editor.focus();
editor.undoManager.transact(() => {
editor.setContent(html);
});
editor.selection.setCursorLocation();
editor.nodeChanged();
};
const getContent = (editor) => {
return editor.getContent({ source_view: true });
};
const open = (editor) => {
const editorContent = getContent(editor);
editor.windowManager.open({
title: 'Source Code',
size: 'large',
body: {
type: 'panel',
items: [
{
type: 'textarea',
name: 'code',
spellcheck: false,
}
]
},
buttons: [
{
type: 'cancel',
name: 'cancel',
text: 'Cancel'
},
{
type: 'submit',
name: 'save',
text: 'Save',
primary: true
}
],
initialData: {
code: editorContent
},
onSubmit: (api) => {
setContent(editor, api.getData().code);
api.close();
}
});
};
const register$1 = (editor) => {
editor.addCommand('mceCodeEditor', () => {
open(editor);
});
};
const register = (editor) => {
const onAction = () => editor.execCommand('mceCodeEditor');
editor.ui.registry.addButton('code', {
icon: 'sourcecode',
tooltip: 'Source code',
onAction
});
editor.ui.registry.addMenuItem('code', {
icon: 'sourcecode',
text: 'Source code',
onAction
});
};
var Plugin = () => {
global.add('code', (editor) => {
register$1(editor);
register(editor);
return {};
});
};
Plugin();
/** *****
* DO NOT EXPORT ANYTHING
*
* IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE
*******/
})();
+4 -1
View File
@@ -1 +1,4 @@
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("code",e=>((e=>{e.addCommand("mceCodeEditor",()=>{(e=>{const o=(e=>e.getContent({source_view:!0}))(e);e.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code",spellcheck:!1}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:o},onSubmit:o=>{((e,o)=>{e.focus(),e.undoManager.transact(()=>{e.setContent(o)}),e.selection.setCursorLocation(),e.nodeChanged()})(e,o.getData().code),o.close()}})})(e)})})(e),(e=>{const o=()=>e.execCommand("mceCodeEditor");e.ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:o}),e.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:o})})(e),{}))}();
/**
* TinyMCE version 7.3.0 (2024-08-07)
*/
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("code",(e=>((e=>{e.addCommand("mceCodeEditor",(()=>{(e=>{const o=(e=>e.getContent({source_view:!0}))(e);e.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:o},onSubmit:o=>{((e,o)=>{e.focus(),e.undoManager.transact((()=>{e.setContent(o)})),e.selection.setCursorLocation(),e.nodeChanged()})(e,o.getData().code),o.close()}})})(e)}))})(e),(e=>{const o=()=>e.execCommand("mceCodeEditor");e.ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:o}),e.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:o})})(e),{})))}();
-7
View File
@@ -1,7 +0,0 @@
// Exports the "codesample" plugin for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/plugins/codesample')
// ES2015:
// import 'tinymce/plugins/codesample'
require('./plugin.js');
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -1,7 +0,0 @@
// Exports the "directionality" plugin for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/plugins/directionality')
// ES2015:
// import 'tinymce/plugins/directionality'
require('./plugin.js');
@@ -1,670 +0,0 @@
/**
* TinyMCE version 8.5.1 (2026-05-19)
*/
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
/* eslint-disable @typescript-eslint/no-wrapper-object-types */
const hasProto = (v, constructor, predicate) => {
if (predicate(v, constructor.prototype)) {
return true;
}
else {
// String-based fallback time
return v.constructor?.name === constructor.name;
}
};
const typeOf = (x) => {
const t = typeof x;
if (x === null) {
return 'null';
}
else if (t === 'object' && Array.isArray(x)) {
return 'array';
}
else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) {
return 'string';
}
else {
return t;
}
};
const isType$1 = (type) => (value) => typeOf(value) === type;
const isSimpleType = (type) => (value) => typeof value === type;
const isString = isType$1('string');
const isBoolean = isSimpleType('boolean');
const isNullable = (a) => a === null || a === undefined;
const isNonNullable = (a) => !isNullable(a);
const isFunction = isSimpleType('function');
const isNumber = isSimpleType('number');
/** Compose two unary functions. Similar to compose, but avoids using Function.prototype.apply. */
const compose1 = (fbc, fab) => (a) => fbc(fab(a));
const constant = (value) => {
return () => {
return value;
};
};
const tripleEquals = (a, b) => {
return a === b;
};
const never = constant(false);
/**
* The `Optional` type represents a value (of any type) that potentially does
* not exist. Any `Optional<T>` can either be a `Some<T>` (in which case the
* value does exist) or a `None` (in which case the value does not exist). This
* module defines a whole lot of FP-inspired utility functions for dealing with
* `Optional` objects.
*
* Comparison with null or undefined:
* - We don't get fancy null coalescing operators with `Optional`
* - We do get fancy helper functions with `Optional`
* - `Optional` support nesting, and allow for the type to still be nullable (or
* another `Optional`)
* - There is no option to turn off strict-optional-checks like there is for
* strict-null-checks
*/
class Optional {
tag;
value;
// Sneaky optimisation: every instance of Optional.none is identical, so just
// reuse the same object
static singletonNone = new Optional(false);
// The internal representation has a `tag` and a `value`, but both are
// private: able to be console.logged, but not able to be accessed by code
constructor(tag, value) {
this.tag = tag;
this.value = value;
}
// --- Identities ---
/**
* Creates a new `Optional<T>` that **does** contain a value.
*/
static some(value) {
return new Optional(true, value);
}
/**
* Create a new `Optional<T>` that **does not** contain a value. `T` can be
* any type because we don't actually have a `T`.
*/
static none() {
return Optional.singletonNone;
}
/**
* Perform a transform on an `Optional` type. Regardless of whether this
* `Optional` contains a value or not, `fold` will return a value of type `U`.
* If this `Optional` does not contain a value, the `U` will be created by
* calling `onNone`. If this `Optional` does contain a value, the `U` will be
* created by calling `onSome`.
*
* For the FP enthusiasts in the room, this function:
* 1. Could be used to implement all of the functions below
* 2. Forms a catamorphism
*/
fold(onNone, onSome) {
if (this.tag) {
return onSome(this.value);
}
else {
return onNone();
}
}
/**
* Determine if this `Optional` object contains a value.
*/
isSome() {
return this.tag;
}
/**
* Determine if this `Optional` object **does not** contain a value.
*/
isNone() {
return !this.tag;
}
// --- Functor (name stolen from Haskell / maths) ---
/**
* Perform a transform on an `Optional` object, **if** there is a value. If
* you provide a function to turn a T into a U, this is the function you use
* to turn an `Optional<T>` into an `Optional<U>`. If this **does** contain
* a value then the output will also contain a value (that value being the
* output of `mapper(this.value)`), and if this **does not** contain a value
* then neither will the output.
*/
map(mapper) {
if (this.tag) {
return Optional.some(mapper(this.value));
}
else {
return Optional.none();
}
}
// --- Monad (name stolen from Haskell / maths) ---
/**
* Perform a transform on an `Optional` object, **if** there is a value.
* Unlike `map`, here the transform itself also returns an `Optional`.
*/
bind(binder) {
if (this.tag) {
return binder(this.value);
}
else {
return Optional.none();
}
}
// --- Traversable (name stolen from Haskell / maths) ---
/**
* For a given predicate, this function finds out if there **exists** a value
* inside this `Optional` object that meets the predicate. In practice, this
* means that for `Optional`s that do not contain a value it returns false (as
* no predicate-meeting value exists).
*/
exists(predicate) {
return this.tag && predicate(this.value);
}
/**
* For a given predicate, this function finds out if **all** the values inside
* this `Optional` object meet the predicate. In practice, this means that
* for `Optional`s that do not contain a value it returns true (as all 0
* objects do meet the predicate).
*/
forall(predicate) {
return !this.tag || predicate(this.value);
}
filter(predicate) {
if (!this.tag || predicate(this.value)) {
return this;
}
else {
return Optional.none();
}
}
// --- Getters ---
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided `Optional` object does not contain a
* value.
*/
getOr(replacement) {
return this.tag ? this.value : replacement;
}
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided `Optional` object does not contain a
* value. Unlike `getOr`, in this method the `replacement` object is also
* `Optional` - meaning that this method will always return an `Optional`.
*/
or(replacement) {
return this.tag ? this : replacement;
}
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided `Optional` object does not contain a
* value. Unlike `getOr`, in this method the `replacement` value is
* "thunked" - that is to say that you don't pass a value to `getOrThunk`, you
* pass a function which (if called) will **return** the `value` you want to
* use.
*/
getOrThunk(thunk) {
return this.tag ? this.value : thunk();
}
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided Optional object does not contain a
* value.
*
* Unlike `or`, in this method the `replacement` value is "thunked" - that is
* to say that you don't pass a value to `orThunk`, you pass a function which
* (if called) will **return** the `value` you want to use.
*
* Unlike `getOrThunk`, in this method the `replacement` value is also
* `Optional`, meaning that this method will always return an `Optional`.
*/
orThunk(thunk) {
return this.tag ? this : thunk();
}
/**
* Get the value out of the inside of the `Optional` object, throwing an
* exception if the provided `Optional` object does not contain a value.
*
* WARNING:
* You should only be using this function if you know that the `Optional`
* object **is not** empty (otherwise you're throwing exceptions in production
* code, which is bad).
*
* In tests this is more acceptable.
*
* Prefer other methods to this, such as `.each`.
*/
getOrDie(message) {
if (!this.tag) {
throw new Error(message ?? 'Called getOrDie on None');
}
else {
return this.value;
}
}
// --- Interop with null and undefined ---
/**
* Creates an `Optional` value from a nullable (or undefined-able) input.
* Null, or undefined, is converted to `None`, and anything else is converted
* to `Some`.
*/
static from(value) {
return isNonNullable(value) ? Optional.some(value) : Optional.none();
}
/**
* Converts an `Optional` to a nullable type, by getting the value if it
* exists, or returning `null` if it does not.
*/
getOrNull() {
return this.tag ? this.value : null;
}
/**
* Converts an `Optional` to an undefined-able type, by getting the value if
* it exists, or returning `undefined` if it does not.
*/
getOrUndefined() {
return this.value;
}
// --- Utilities ---
/**
* If the `Optional` contains a value, perform an action on that value.
* Unlike the rest of the methods on this type, `.each` has side-effects. If
* you want to transform an `Optional<T>` **into** something, then this is not
* the method for you. If you want to use an `Optional<T>` to **do**
* something, then this is the method for you - provided you're okay with not
* doing anything in the case where the `Optional` doesn't have a value inside
* it. If you're not sure whether your use-case fits into transforming
* **into** something or **doing** something, check whether it has a return
* value. If it does, you should be performing a transform.
*/
each(worker) {
if (this.tag) {
worker(this.value);
}
}
/**
* Turn the `Optional` object into an array that contains all of the values
* stored inside the `Optional`. In practice, this means the output will have
* either 0 or 1 elements.
*/
toArray() {
return this.tag ? [this.value] : [];
}
/**
* Turn the `Optional` object into a string for debugging or printing. Not
* recommended for production code, but good for debugging. Also note that
* these days an `Optional` object can be logged to the console directly, and
* its inner value (if it exists) will be visible.
*/
toString() {
return this.tag ? `some(${this.value})` : 'none()';
}
}
const nativeSlice = Array.prototype.slice;
const map = (xs, f) => {
// pre-allocating array size when it's guaranteed to be known
// http://jsperf.com/push-allocated-vs-dynamic/22
const len = xs.length;
const r = new Array(len);
for (let i = 0; i < len; i++) {
const x = xs[i];
r[i] = f(x, i);
}
return r;
};
// Unwound implementing other functions in terms of each.
// The code size is roughly the same, and it should allow for better optimisation.
// const each = function<T, U>(xs: T[], f: (x: T, i?: number, xs?: T[]) => void): void {
const each = (xs, f) => {
for (let i = 0, len = xs.length; i < len; i++) {
const x = xs[i];
f(x, i);
}
};
const filter = (xs, pred) => {
const r = [];
for (let i = 0, len = xs.length; i < len; i++) {
const x = xs[i];
if (pred(x, i)) {
r.push(x);
}
}
return r;
};
isFunction(Array.from) ? Array.from : (x) => nativeSlice.call(x);
/**
* **Is** the value stored inside this Optional object equal to `rhs`?
*/
const is$1 = (lhs, rhs, comparator = tripleEquals) => lhs.exists((left) => comparator(left, rhs));
const blank = (r) => (s) => s.replace(r, '');
/** removes all leading and trailing spaces */
const trim = blank(/^\s+|\s+$/g);
const isNotEmpty = (s) => s.length > 0;
const fromHtml = (html, scope) => {
const doc = scope || document;
const div = doc.createElement('div');
div.innerHTML = html;
if (!div.hasChildNodes() || div.childNodes.length > 1) {
const message = 'HTML does not have a single root node';
// eslint-disable-next-line no-console
console.error(message, html);
throw new Error(message);
}
return fromDom(div.childNodes[0]);
};
const fromTag = (tag, scope) => {
const doc = scope || document;
const node = doc.createElement(tag);
return fromDom(node);
};
const fromText = (text, scope) => {
const doc = scope || document;
const node = doc.createTextNode(text);
return fromDom(node);
};
const fromDom = (node) => {
// TODO: Consider removing this check, but left atm for safety
if (node === null || node === undefined) {
throw new Error('Node cannot be null or undefined');
}
return {
dom: node
};
};
const fromPoint = (docElm, x, y) => Optional.from(docElm.dom.elementFromPoint(x, y)).map(fromDom);
// tslint:disable-next-line:variable-name
const SugarElement = {
fromHtml,
fromTag,
fromText,
fromDom,
fromPoint
};
const DOCUMENT_FRAGMENT = 11;
const ELEMENT = 1;
const TEXT = 3;
const is = (element, selector) => {
const dom = element.dom;
if (dom.nodeType !== ELEMENT) {
return false;
}
else {
const elem = dom;
if (elem.matches !== undefined) {
return elem.matches(selector);
}
else if (elem.msMatchesSelector !== undefined) {
return elem.msMatchesSelector(selector);
}
else if (elem.webkitMatchesSelector !== undefined) {
return elem.webkitMatchesSelector(selector);
}
else if (elem.mozMatchesSelector !== undefined) {
// cast to any as mozMatchesSelector doesn't exist in TS DOM lib
return elem.mozMatchesSelector(selector);
}
else {
throw new Error('Browser lacks native selectors');
} // unfortunately we can't throw this on startup :(
}
};
const name = (element) => {
const r = element.dom.nodeName;
return r.toLowerCase();
};
const type = (element) => element.dom.nodeType;
const isType = (t) => (element) => type(element) === t;
const isElement = isType(ELEMENT);
const isText = isType(TEXT);
const isDocumentFragment = isType(DOCUMENT_FRAGMENT);
const isTag = (tag) => (e) => isElement(e) && name(e) === tag;
const children$2 = (element) => map(element.dom.childNodes, SugarElement.fromDom);
/**
* Is the element a ShadowRoot?
*
* Note: this is insufficient to test if any element is a shadow root, but it is sufficient to differentiate between
* a Document and a ShadowRoot.
*/
const isShadowRoot = (dos) => isDocumentFragment(dos) && isNonNullable(dos.dom.host);
const getRootNode = (e) => SugarElement.fromDom(e.dom.getRootNode());
/** If this element is in a ShadowRoot, return it. */
const getShadowRoot = (e) => {
const r = getRootNode(e);
return isShadowRoot(r) ? Optional.some(r) : Optional.none();
};
/** Return the host of a ShadowRoot.
*
* This function will throw if Shadow DOM is unsupported in the browser, or if the host is null.
* If you actually have a ShadowRoot, this shouldn't happen.
*/
const getShadowHost = (e) => SugarElement.fromDom(e.dom.host);
const rawSet = (dom, key, value) => {
/*
* JQuery coerced everything to a string, and silently did nothing on text node/null/undefined.
*
* We fail on those invalid cases, only allowing numbers and booleans.
*/
if (isString(value) || isBoolean(value) || isNumber(value)) {
dom.setAttribute(key, value + '');
}
else {
// eslint-disable-next-line no-console
console.error('Invalid call to Attribute.set. Key ', key, ':: Value ', value, ':: Element ', dom);
throw new Error('Attribute value was not simple');
}
};
const set = (element, key, value) => {
rawSet(element.dom, key, value);
};
const get$1 = (element, key) => {
const v = element.dom.getAttribute(key);
// undefined is the more appropriate value for JS, and this matches JQuery
return v === null ? undefined : v;
};
const getOpt = (element, key) => Optional.from(get$1(element, key));
const remove$1 = (element, key) => {
element.dom.removeAttribute(key);
};
// some elements, such as mathml, don't have style attributes
// others, such as angular elements, have style attributes that aren't a CSSStyleDeclaration
const isSupported = (dom) => dom.style !== undefined && isFunction(dom.style.getPropertyValue);
// Node.contains() is very, very, very good performance
// http://jsperf.com/closest-vs-contains/5
const inBody = (element) => {
// Technically this is only required on IE, where contains() returns false for text nodes.
// But it's cheap enough to run everywhere and Sugar doesn't have platform detection (yet).
const dom = isText(element) ? element.dom.parentNode : element.dom;
// use ownerDocument.body to ensure this works inside iframes.
// Normally contains is bad because an element "contains" itself, but here we want that.
if (dom === undefined || dom === null || dom.ownerDocument === null) {
return false;
}
const doc = dom.ownerDocument;
return getShadowRoot(SugarElement.fromDom(dom)).fold(() => doc.body.contains(dom), compose1(inBody, getShadowHost));
};
const internalRemove = (dom, property) => {
/*
* IE9 and above - MDN doesn't have details, but here's a couple of random internet claims
*
* http://help.dottoro.com/ljopsjck.php
* http://stackoverflow.com/a/7901886/7546
*/
if (isSupported(dom)) {
dom.style.removeProperty(property);
}
};
/*
* NOTE: For certain properties, this returns the "used value" which is subtly different to the "computed value" (despite calling getComputedStyle).
* Blame CSS 2.0.
*
* https://developer.mozilla.org/en-US/docs/Web/CSS/used_value
*/
const get = (element, property) => {
const dom = element.dom;
/*
* IE9 and above per
* https://developer.mozilla.org/en/docs/Web/API/window.getComputedStyle
*
* Not in numerosity, because it doesn't memoize and looking this up dynamically in performance critical code would be horrendous.
*
* JQuery has some magic here for IE popups, but we don't really need that.
* It also uses element.ownerDocument.defaultView to handle iframes but that hasn't been required since FF 3.6.
*/
const styles = window.getComputedStyle(dom);
const r = styles.getPropertyValue(property);
// jquery-ism: If r is an empty string, check that the element is not in a document. If it isn't, return the raw value.
// Turns out we do this a lot.
return (r === '' && !inBody(element)) ? getUnsafeProperty(dom, property) : r;
};
// removed: support for dom().style[property] where prop is camel case instead of normal property name
// empty string is what the browsers (IE11 and Chrome) return when the propertyValue doesn't exists.
const getUnsafeProperty = (dom, property) => isSupported(dom) ? dom.style.getPropertyValue(property) : '';
const remove = (element, property) => {
const dom = element.dom;
internalRemove(dom, property);
if (is$1(getOpt(element, 'style').map(trim), '')) {
// No more styles left, remove the style attribute as well
remove$1(element, 'style');
}
};
const getDirection = (element) => get(element, 'direction') === 'rtl' ? 'rtl' : 'ltr';
const ancestor$1 = (scope, predicate, isRoot) => {
let element = scope.dom;
const stop = isFunction(isRoot) ? isRoot : never;
while (element.parentNode) {
element = element.parentNode;
const el = SugarElement.fromDom(element);
if (predicate(el)) {
return Optional.some(el);
}
else if (stop(el)) {
break;
}
}
return Optional.none();
};
const ancestor = (scope, selector, isRoot) => ancestor$1(scope, (e) => is(e, selector), isRoot);
const children$1 = (scope, predicate) => filter(children$2(scope), predicate);
const children = (scope, selector) =>
// It may surprise you to learn this is exactly what JQuery does
// TODO: Avoid all the wrapping and unwrapping
children$1(scope, (e) => is(e, selector));
// if the block is a list item, we need to get the parent of the list itself
const getNormalizedBlock = (element, isListItem) => {
const normalizedElement = isListItem ? ancestor(element, 'ol,ul') : Optional.some(element);
return normalizedElement.getOr(element);
};
const isListItem = isTag('li');
const setDirOnElements = (dom, blocks, dir) => {
each(blocks, (block) => {
const blockElement = SugarElement.fromDom(block);
const isBlockElementListItem = isListItem(blockElement);
const normalizedBlock = getNormalizedBlock(blockElement, isBlockElementListItem);
const attrDir = get$1(normalizedBlock, 'dir');
const styleDir = Optional.from(dom.getStyle(normalizedBlock.dom, 'direction', false))
.filter(isNotEmpty);
if (attrDir === dir || styleDir.exists((d) => d === dir)) {
// If already set to given dir, toggle dir off
remove$1(normalizedBlock, 'dir');
remove(normalizedBlock, 'direction');
}
else if (attrDir !== dir) {
set(normalizedBlock, 'dir', dir); // Set the dir attribute to the desired direction
if (styleDir.exists((d) => d !== dir) || getDirection(normalizedBlock) !== dir) {
// Also set the inline direction style if it was already set or if necessary to override inherited direction
dom.setStyle(normalizedBlock.dom, 'direction', dir);
}
}
// Remove dir attr and direction style from list children
if (isBlockElementListItem) {
const listItems = children(normalizedBlock, 'li[dir],li[style]');
each(listItems, (listItem) => {
remove$1(listItem, 'dir');
dom.setStyle(listItem.dom, 'direction', null);
});
}
});
};
const setDir = (editor, dir) => {
if (editor.selection.isEditable()) {
setDirOnElements(editor.dom, editor.selection.getSelectedBlocks(), dir);
editor.nodeChanged();
}
};
const register$1 = (editor) => {
editor.addCommand('mceDirectionLTR', () => {
setDir(editor, 'ltr');
});
editor.addCommand('mceDirectionRTL', () => {
setDir(editor, 'rtl');
});
};
const register = (editor) => {
const setupHandler = (dir) => (api) => {
const nodeChangeHandler = (e) => {
const block = editor.dom.isBlock(e.element) ? e.element : editor.dom.getParent(e.element, editor.dom.isBlock);
if (block) {
const direction = editor.dom.getStyle(block, 'direction') || editor.dom.getAttrib(block, 'dir');
api.setActive(direction === dir);
}
};
editor.on('NodeChange', nodeChangeHandler);
return () => editor.off('NodeChange', nodeChangeHandler);
};
editor.ui.registry.addToggleButton('ltr', {
tooltip: 'Left to right',
icon: 'ltr',
context: 'editable',
onAction: () => editor.execCommand('mceDirectionLTR'),
onSetup: setupHandler('ltr'),
});
editor.ui.registry.addToggleButton('rtl', {
tooltip: 'Right to left',
icon: 'rtl',
context: 'editable',
onAction: () => editor.execCommand('mceDirectionRTL'),
onSetup: setupHandler('rtl')
});
};
var Plugin = () => {
global.add('directionality', (editor) => {
register$1(editor);
register(editor);
});
};
Plugin();
/** *****
* DO NOT EXPORT ANYTHING
*
* IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE
*******/
})();
+4 -1
View File
@@ -1 +1,4 @@
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=t=>e=>typeof e===t,r=t=>"string"===(t=>{const e=typeof t;return null===t?"null":"object"===e&&Array.isArray(t)?"array":"object"===e&&(r=o=t,(n=String).prototype.isPrototypeOf(r)||o.constructor?.name===n.name)?"string":e;var r,o,n})(t),o=e("boolean"),n=t=>!(t=>null==t)(t),i=e("function"),s=e("number"),l=(t,e)=>t===e,a=()=>false;class c{tag;value;static singletonNone=new c(!1);constructor(t,e){this.tag=t,this.value=e}static some(t){return new c(!0,t)}static none(){return c.singletonNone}fold(t,e){return this.tag?e(this.value):t()}isSome(){return this.tag}isNone(){return!this.tag}map(t){return this.tag?c.some(t(this.value)):c.none()}bind(t){return this.tag?t(this.value):c.none()}exists(t){return this.tag&&t(this.value)}forall(t){return!this.tag||t(this.value)}filter(t){return!this.tag||t(this.value)?this:c.none()}getOr(t){return this.tag?this.value:t}or(t){return this.tag?this:t}getOrThunk(t){return this.tag?this.value:t()}orThunk(t){return this.tag?this:t()}getOrDie(t){if(this.tag)return this.value;throw new Error(t??"Called getOrDie on None")}static from(t){return n(t)?c.some(t):c.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(t){this.tag&&t(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}Array.prototype.slice;const u=(t,e)=>{for(let r=0,o=t.length;r<o;r++)e(t[r],r)};i(Array.from)&&Array.from;const d=(m=/^\s+|\s+$/g,t=>t.replace(m,""));var m;const h=t=>t.length>0,g=t=>{if(null==t)throw new Error("Node cannot be null or undefined");return{dom:t}},f=g,y=(t,e)=>{const r=t.dom;if(1!==r.nodeType)return!1;{const t=r;if(void 0!==t.matches)return t.matches(e);if(void 0!==t.msMatchesSelector)return t.msMatchesSelector(e);if(void 0!==t.webkitMatchesSelector)return t.webkitMatchesSelector(e);if(void 0!==t.mozMatchesSelector)return t.mozMatchesSelector(e);throw new Error("Browser lacks native selectors")}},v=t=>e=>(t=>t.dom.nodeType)(e)===t,p=v(1),w=v(3),b=v(11),S=t=>f(t.dom.host),A=(t,e)=>{const r=t.dom.getAttribute(e);return null===r?void 0:r},N=(t,e)=>{t.dom.removeAttribute(e)},T=t=>void 0!==t.style&&i(t.style.getPropertyValue),C=t=>{const e=w(t)?t.dom.parentNode:t.dom;if(null==e||null===e.ownerDocument)return!1;const r=e.ownerDocument;return(t=>{const e=(t=>f(t.dom.getRootNode()))(t);return b(r=e)&&n(r.dom.host)?c.some(e):c.none();var r})(f(e)).fold(()=>r.body.contains(e),(o=C,i=S,t=>o(i(t))));var o,i},k=t=>"rtl"===((t,e)=>{const r=t.dom,o=window.getComputedStyle(r).getPropertyValue(e);return""!==o||C(t)?o:((t,e)=>T(t)?t.style.getPropertyValue(e):"")(r,e)})(t,"direction")?"rtl":"ltr",x=(t,e)=>(t=>((t,e)=>{const r=[];for(let o=0,n=t.length;o<n;o++){const n=t[o];e(n,o)&&r.push(n)}return r})(((t,e)=>{const r=t.length,o=new Array(r);for(let n=0;n<r;n++){const r=t[n];o[n]=e(r,n)}return o})(t.dom.childNodes,f),t=>y(t,e)))(t),D=t=>p(t)&&"li"===t.dom.nodeName.toLowerCase();const O=(t,e,n)=>{u(e,e=>{const m=f(e),g=D(m),v=((t,e)=>{return(e?(r=t,o="ol,ul",((t,e,r)=>{let o=t.dom;const n=i(r)?r:a;for(;o.parentNode;){o=o.parentNode;const t=f(o);if(e(t))return c.some(t);if(n(t))break}return c.none()})(r,t=>y(t,o),n)):c.some(t)).getOr(t);var r,o,n})(m,g),p=A(v,"dir"),w=c.from(t.getStyle(v.dom,"direction",!1)).filter(h);if(p===n||w.exists(t=>t===n)?(N(v,"dir"),S="direction",((t,e)=>{T(t)&&t.style.removeProperty(e)})((b=v).dom,S),((t,e,r=l)=>t.exists(t=>r(t,e)))((t=>c.from(A(t,"style")))(b).map(d),"")&&N(b,"style")):p!==n&&(((t,e,n)=>{((t,e,n)=>{if(!(r(n)||o(n)||s(n)))throw console.error("Invalid call to Attribute.set. Key ",e,":: Value ",n,":: Element ",t),new Error("Attribute value was not simple");t.setAttribute(e,n+"")})(t.dom,e,n)})(v,"dir",n),(w.exists(t=>t!==n)||k(v)!==n)&&t.setStyle(v.dom,"direction",n)),g){const e=x(v,"li[dir],li[style]");u(e,e=>{N(e,"dir"),t.setStyle(e.dom,"direction",null)})}var b,S})},M=(t,e)=>{t.selection.isEditable()&&(O(t.dom,t.selection.getSelectedBlocks(),e),t.nodeChanged())};t.add("directionality",t=>{(t=>{t.addCommand("mceDirectionLTR",()=>{M(t,"ltr")}),t.addCommand("mceDirectionRTL",()=>{M(t,"rtl")})})(t),(t=>{const e=e=>r=>{const o=o=>{const n=t.dom.isBlock(o.element)?o.element:t.dom.getParent(o.element,t.dom.isBlock);if(n){const o=t.dom.getStyle(n,"direction")||t.dom.getAttrib(n,"dir");r.setActive(o===e)}};return t.on("NodeChange",o),()=>t.off("NodeChange",o)};t.ui.registry.addToggleButton("ltr",{tooltip:"Left to right",icon:"ltr",context:"editable",onAction:()=>t.execCommand("mceDirectionLTR"),onSetup:e("ltr")}),t.ui.registry.addToggleButton("rtl",{tooltip:"Right to left",icon:"rtl",context:"editable",onAction:()=>t.execCommand("mceDirectionRTL"),onSetup:e("rtl")})})(t)})}();
/**
* TinyMCE version 7.3.0 (2024-08-07)
*/
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=t=>e=>typeof e===t,o=t=>"string"===(t=>{const e=typeof t;return null===t?"null":"object"===e&&Array.isArray(t)?"array":"object"===e&&(o=r=t,(n=String).prototype.isPrototypeOf(o)||(null===(i=r.constructor)||void 0===i?void 0:i.name)===n.name)?"string":e;var o,r,n,i})(t),r=e("boolean"),n=t=>!(t=>null==t)(t),i=e("function"),s=e("number"),l=(!1,()=>false);class a{constructor(t,e){this.tag=t,this.value=e}static some(t){return new a(!0,t)}static none(){return a.singletonNone}fold(t,e){return this.tag?e(this.value):t()}isSome(){return this.tag}isNone(){return!this.tag}map(t){return this.tag?a.some(t(this.value)):a.none()}bind(t){return this.tag?t(this.value):a.none()}exists(t){return this.tag&&t(this.value)}forall(t){return!this.tag||t(this.value)}filter(t){return!this.tag||t(this.value)?this:a.none()}getOr(t){return this.tag?this.value:t}or(t){return this.tag?this:t}getOrThunk(t){return this.tag?this.value:t()}orThunk(t){return this.tag?this:t()}getOrDie(t){if(this.tag)return this.value;throw new Error(null!=t?t:"Called getOrDie on None")}static from(t){return n(t)?a.some(t):a.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(t){this.tag&&t(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}a.singletonNone=new a(!1);const u=(t,e)=>{for(let o=0,r=t.length;o<r;o++)e(t[o],o)},c=t=>{if(null==t)throw new Error("Node cannot be null or undefined");return{dom:t}},d=c,h=(t,e)=>{const o=t.dom;if(1!==o.nodeType)return!1;{const t=o;if(void 0!==t.matches)return t.matches(e);if(void 0!==t.msMatchesSelector)return t.msMatchesSelector(e);if(void 0!==t.webkitMatchesSelector)return t.webkitMatchesSelector(e);if(void 0!==t.mozMatchesSelector)return t.mozMatchesSelector(e);throw new Error("Browser lacks native selectors")}};"undefined"!=typeof window?window:Function("return this;")();const m=t=>e=>(t=>t.dom.nodeType)(e)===t,g=m(1),f=m(3),v=m(11),y=(t,e)=>{t.dom.removeAttribute(e)},p=t=>d(t.dom.host),w=t=>{const e=f(t)?t.dom.parentNode:t.dom;if(null==e||null===e.ownerDocument)return!1;const o=e.ownerDocument;return(t=>{const e=(t=>d(t.dom.getRootNode()))(t);return v(o=e)&&n(o.dom.host)?a.some(e):a.none();var o})(d(e)).fold((()=>o.body.contains(e)),(r=w,i=p,t=>r(i(t))));var r,i},b=t=>"rtl"===((t,e)=>{const o=t.dom,r=window.getComputedStyle(o).getPropertyValue(e);return""!==r||w(t)?r:((t,e)=>(t=>void 0!==t.style&&i(t.style.getPropertyValue))(t)?t.style.getPropertyValue(e):"")(o,e)})(t,"direction")?"rtl":"ltr",S=(t,e)=>((t,o)=>((t,e)=>{const o=[];for(let r=0,n=t.length;r<n;r++){const n=t[r];e(n,r)&&o.push(n)}return o})(((t,e)=>{const o=t.length,r=new Array(o);for(let n=0;n<o;n++){const o=t[n];r[n]=e(o,n)}return r})(t.dom.childNodes,d),(t=>h(t,e))))(t),N=("li",t=>g(t)&&"li"===t.dom.nodeName.toLowerCase());const A=(t,e,n)=>{u(e,(e=>{const c=d(e),m=N(c),f=((t,e)=>{return(e?(o=t,r="ol,ul",((t,e,o)=>{let n=t.dom;const s=i(o)?o:l;for(;n.parentNode;){n=n.parentNode;const t=d(n);if(h(t,r))return a.some(t);if(s(t))break}return a.none()})(o,0,n)):a.some(t)).getOr(t);var o,r,n})(c,m);var v;(v=f,(t=>a.from(t.dom.parentNode).map(d))(v).filter(g)).each((e=>{if(t.setStyle(f.dom,"direction",null),b(e)===n?y(f,"dir"):((t,e,n)=>{((t,e,n)=>{if(!(o(n)||r(n)||s(n)))throw console.error("Invalid call to Attribute.set. Key ",e,":: Value ",n,":: Element ",t),new Error("Attribute value was not simple");t.setAttribute(e,n+"")})(t.dom,e,n)})(f,"dir",n),b(f)!==n&&t.setStyle(f.dom,"direction",n),m){const e=S(f,"li[dir],li[style]");u(e,(e=>{y(e,"dir"),t.setStyle(e.dom,"direction",null)}))}}))}))},T=(t,e)=>{t.selection.isEditable()&&(A(t.dom,t.selection.getSelectedBlocks(),e),t.nodeChanged())},C=(t,e)=>o=>{const r=r=>{const n=d(r.element);o.setActive(b(n)===e),o.setEnabled(t.selection.isEditable())};return t.on("NodeChange",r),o.setEnabled(t.selection.isEditable()),()=>t.off("NodeChange",r)};t.add("directionality",(t=>{(t=>{t.addCommand("mceDirectionLTR",(()=>{T(t,"ltr")})),t.addCommand("mceDirectionRTL",(()=>{T(t,"rtl")}))})(t),(t=>{t.ui.registry.addToggleButton("ltr",{tooltip:"Left to right",icon:"ltr",onAction:()=>t.execCommand("mceDirectionLTR"),onSetup:C(t,"ltr")}),t.ui.registry.addToggleButton("rtl",{tooltip:"Right to left",icon:"rtl",onAction:()=>t.execCommand("mceDirectionRTL"),onSetup:C(t,"rtl")})})(t)}))}();
-7
View File
@@ -1,7 +0,0 @@
// Exports the "emoticons" plugin for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/plugins/emoticons')
// ES2015:
// import 'tinymce/plugins/emoticons'
require('./plugin.js');
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-809
View File
@@ -1,809 +0,0 @@
/**
* TinyMCE version 8.5.1 (2026-05-19)
*/
(function () {
'use strict';
var global$1 = tinymce.util.Tools.resolve('tinymce.PluginManager');
/* eslint-disable @typescript-eslint/no-wrapper-object-types */
const isSimpleType = (type) => (value) => typeof value === type;
const eq = (t) => (a) => t === a;
const isNull = eq(null);
const isUndefined = eq(undefined);
const isNullable = (a) => a === null || a === undefined;
const isNonNullable = (a) => !isNullable(a);
const isFunction = isSimpleType('function');
const noop = () => { };
const constant = (value) => {
return () => {
return value;
};
};
const never = constant(false);
/**
* The `Optional` type represents a value (of any type) that potentially does
* not exist. Any `Optional<T>` can either be a `Some<T>` (in which case the
* value does exist) or a `None` (in which case the value does not exist). This
* module defines a whole lot of FP-inspired utility functions for dealing with
* `Optional` objects.
*
* Comparison with null or undefined:
* - We don't get fancy null coalescing operators with `Optional`
* - We do get fancy helper functions with `Optional`
* - `Optional` support nesting, and allow for the type to still be nullable (or
* another `Optional`)
* - There is no option to turn off strict-optional-checks like there is for
* strict-null-checks
*/
class Optional {
tag;
value;
// Sneaky optimisation: every instance of Optional.none is identical, so just
// reuse the same object
static singletonNone = new Optional(false);
// The internal representation has a `tag` and a `value`, but both are
// private: able to be console.logged, but not able to be accessed by code
constructor(tag, value) {
this.tag = tag;
this.value = value;
}
// --- Identities ---
/**
* Creates a new `Optional<T>` that **does** contain a value.
*/
static some(value) {
return new Optional(true, value);
}
/**
* Create a new `Optional<T>` that **does not** contain a value. `T` can be
* any type because we don't actually have a `T`.
*/
static none() {
return Optional.singletonNone;
}
/**
* Perform a transform on an `Optional` type. Regardless of whether this
* `Optional` contains a value or not, `fold` will return a value of type `U`.
* If this `Optional` does not contain a value, the `U` will be created by
* calling `onNone`. If this `Optional` does contain a value, the `U` will be
* created by calling `onSome`.
*
* For the FP enthusiasts in the room, this function:
* 1. Could be used to implement all of the functions below
* 2. Forms a catamorphism
*/
fold(onNone, onSome) {
if (this.tag) {
return onSome(this.value);
}
else {
return onNone();
}
}
/**
* Determine if this `Optional` object contains a value.
*/
isSome() {
return this.tag;
}
/**
* Determine if this `Optional` object **does not** contain a value.
*/
isNone() {
return !this.tag;
}
// --- Functor (name stolen from Haskell / maths) ---
/**
* Perform a transform on an `Optional` object, **if** there is a value. If
* you provide a function to turn a T into a U, this is the function you use
* to turn an `Optional<T>` into an `Optional<U>`. If this **does** contain
* a value then the output will also contain a value (that value being the
* output of `mapper(this.value)`), and if this **does not** contain a value
* then neither will the output.
*/
map(mapper) {
if (this.tag) {
return Optional.some(mapper(this.value));
}
else {
return Optional.none();
}
}
// --- Monad (name stolen from Haskell / maths) ---
/**
* Perform a transform on an `Optional` object, **if** there is a value.
* Unlike `map`, here the transform itself also returns an `Optional`.
*/
bind(binder) {
if (this.tag) {
return binder(this.value);
}
else {
return Optional.none();
}
}
// --- Traversable (name stolen from Haskell / maths) ---
/**
* For a given predicate, this function finds out if there **exists** a value
* inside this `Optional` object that meets the predicate. In practice, this
* means that for `Optional`s that do not contain a value it returns false (as
* no predicate-meeting value exists).
*/
exists(predicate) {
return this.tag && predicate(this.value);
}
/**
* For a given predicate, this function finds out if **all** the values inside
* this `Optional` object meet the predicate. In practice, this means that
* for `Optional`s that do not contain a value it returns true (as all 0
* objects do meet the predicate).
*/
forall(predicate) {
return !this.tag || predicate(this.value);
}
filter(predicate) {
if (!this.tag || predicate(this.value)) {
return this;
}
else {
return Optional.none();
}
}
// --- Getters ---
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided `Optional` object does not contain a
* value.
*/
getOr(replacement) {
return this.tag ? this.value : replacement;
}
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided `Optional` object does not contain a
* value. Unlike `getOr`, in this method the `replacement` object is also
* `Optional` - meaning that this method will always return an `Optional`.
*/
or(replacement) {
return this.tag ? this : replacement;
}
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided `Optional` object does not contain a
* value. Unlike `getOr`, in this method the `replacement` value is
* "thunked" - that is to say that you don't pass a value to `getOrThunk`, you
* pass a function which (if called) will **return** the `value` you want to
* use.
*/
getOrThunk(thunk) {
return this.tag ? this.value : thunk();
}
/**
* Get the value out of the inside of the `Optional` object, using a default
* `replacement` value if the provided Optional object does not contain a
* value.
*
* Unlike `or`, in this method the `replacement` value is "thunked" - that is
* to say that you don't pass a value to `orThunk`, you pass a function which
* (if called) will **return** the `value` you want to use.
*
* Unlike `getOrThunk`, in this method the `replacement` value is also
* `Optional`, meaning that this method will always return an `Optional`.
*/
orThunk(thunk) {
return this.tag ? this : thunk();
}
/**
* Get the value out of the inside of the `Optional` object, throwing an
* exception if the provided `Optional` object does not contain a value.
*
* WARNING:
* You should only be using this function if you know that the `Optional`
* object **is not** empty (otherwise you're throwing exceptions in production
* code, which is bad).
*
* In tests this is more acceptable.
*
* Prefer other methods to this, such as `.each`.
*/
getOrDie(message) {
if (!this.tag) {
throw new Error(message ?? 'Called getOrDie on None');
}
else {
return this.value;
}
}
// --- Interop with null and undefined ---
/**
* Creates an `Optional` value from a nullable (or undefined-able) input.
* Null, or undefined, is converted to `None`, and anything else is converted
* to `Some`.
*/
static from(value) {
return isNonNullable(value) ? Optional.some(value) : Optional.none();
}
/**
* Converts an `Optional` to a nullable type, by getting the value if it
* exists, or returning `null` if it does not.
*/
getOrNull() {
return this.tag ? this.value : null;
}
/**
* Converts an `Optional` to an undefined-able type, by getting the value if
* it exists, or returning `undefined` if it does not.
*/
getOrUndefined() {
return this.value;
}
// --- Utilities ---
/**
* If the `Optional` contains a value, perform an action on that value.
* Unlike the rest of the methods on this type, `.each` has side-effects. If
* you want to transform an `Optional<T>` **into** something, then this is not
* the method for you. If you want to use an `Optional<T>` to **do**
* something, then this is the method for you - provided you're okay with not
* doing anything in the case where the `Optional` doesn't have a value inside
* it. If you're not sure whether your use-case fits into transforming
* **into** something or **doing** something, check whether it has a return
* value. If it does, you should be performing a transform.
*/
each(worker) {
if (this.tag) {
worker(this.value);
}
}
/**
* Turn the `Optional` object into an array that contains all of the values
* stored inside the `Optional`. In practice, this means the output will have
* either 0 or 1 elements.
*/
toArray() {
return this.tag ? [this.value] : [];
}
/**
* Turn the `Optional` object into a string for debugging or printing. Not
* recommended for production code, but good for debugging. Also note that
* these days an `Optional` object can be logged to the console directly, and
* its inner value (if it exists) will be visible.
*/
toString() {
return this.tag ? `some(${this.value})` : 'none()';
}
}
const nativeSlice = Array.prototype.slice;
const exists = (xs, pred) => {
for (let i = 0, len = xs.length; i < len; i++) {
const x = xs[i];
if (pred(x, i)) {
return true;
}
}
return false;
};
const map$1 = (xs, f) => {
// pre-allocating array size when it's guaranteed to be known
// http://jsperf.com/push-allocated-vs-dynamic/22
const len = xs.length;
const r = new Array(len);
for (let i = 0; i < len; i++) {
const x = xs[i];
r[i] = f(x, i);
}
return r;
};
// Unwound implementing other functions in terms of each.
// The code size is roughly the same, and it should allow for better optimisation.
// const each = function<T, U>(xs: T[], f: (x: T, i?: number, xs?: T[]) => void): void {
const each$1 = (xs, f) => {
for (let i = 0, len = xs.length; i < len; i++) {
const x = xs[i];
f(x, i);
}
};
isFunction(Array.from) ? Array.from : (x) => nativeSlice.call(x);
// There are many variations of Object iteration that are faster than the 'for-in' style:
// http://jsperf.com/object-keys-iteration/107
//
// Use the native keys if it is available (IE9+), otherwise fall back to manually filtering
const keys = Object.keys;
const hasOwnProperty = Object.hasOwnProperty;
const each = (obj, f) => {
const props = keys(obj);
for (let k = 0, len = props.length; k < len; k++) {
const i = props[k];
const x = obj[i];
f(x, i);
}
};
const map = (obj, f) => {
return tupleMap(obj, (x, i) => ({
k: i,
v: f(x, i)
}));
};
const tupleMap = (obj, f) => {
const r = {};
each(obj, (x, i) => {
const tuple = f(x, i);
r[tuple.k] = tuple.v;
});
return r;
};
const has = (obj, key) => hasOwnProperty.call(obj, key);
const Cell = (initial) => {
let value = initial;
const get = () => {
return value;
};
const set = (v) => {
value = v;
};
return {
get,
set
};
};
const shallow = (old, nu) => {
return nu;
};
const baseMerge = (merger) => {
return (...objects) => {
if (objects.length === 0) {
throw new Error(`Can't merge zero objects`);
}
const ret = {};
for (let j = 0; j < objects.length; j++) {
const curObject = objects[j];
for (const key in curObject) {
if (has(curObject, key)) {
ret[key] = merger(ret[key], curObject[key]);
}
}
}
return ret;
};
};
const merge = baseMerge(shallow);
const singleton = (doRevoke) => {
const subject = Cell(Optional.none());
const revoke = () => subject.get().each(doRevoke);
const clear = () => {
revoke();
subject.set(Optional.none());
};
const isSet = () => subject.get().isSome();
const get = () => subject.get();
const set = (s) => {
revoke();
subject.set(Optional.some(s));
};
return {
clear,
isSet,
get,
set
};
};
const value = () => {
const subject = singleton(noop);
const on = (f) => subject.get().each(f);
return {
...subject,
on
};
};
const checkRange = (str, substr, start) => substr === '' || str.length >= substr.length && str.substr(start, start + substr.length) === substr;
const contains = (str, substr, start = 0, end) => {
const idx = str.indexOf(substr, start);
if (idx !== -1) {
return isUndefined(end) ? true : idx + substr.length <= end;
}
else {
return false;
}
};
/** Does 'str' start with 'prefix'?
* Note: all strings start with the empty string.
* More formally, for all strings x, startsWith(x, "").
* This is so that for all strings x and y, startsWith(y + x, y)
*/
const startsWith = (str, prefix) => {
return checkRange(str, prefix, 0);
};
// Run a function fn after rate ms. If another invocation occurs
// during the time it is waiting, reschedule the function again
// with the new arguments.
const last = (fn, rate) => {
let timer = null;
const cancel = () => {
if (!isNull(timer)) {
clearTimeout(timer);
timer = null;
}
};
const throttle = (...args) => {
cancel();
timer = setTimeout(() => {
timer = null;
fn.apply(null, args);
}, rate);
};
return {
cancel,
throttle
};
};
const insertEmoticon = (editor, ch) => {
editor.insertContent(ch);
};
var global = tinymce.util.Tools.resolve('tinymce.Resource');
const DEFAULT_ID = 'tinymce.plugins.emoticons';
const option = (name) => (editor) => editor.options.get(name);
const register$2 = (editor, pluginUrl) => {
const registerOption = editor.options.register;
registerOption('emoticons_database', {
processor: 'string',
default: 'emojis'
});
registerOption('emoticons_database_url', {
processor: 'string',
default: `${pluginUrl}/js/${getEmojiDatabase(editor)}${editor.suffix}.js`
});
registerOption('emoticons_database_id', {
processor: 'string',
default: DEFAULT_ID
});
registerOption('emoticons_append', {
processor: 'object',
default: {}
});
registerOption('emoticons_images_url', {
processor: 'string',
default: 'https://cdnjs.cloudflare.com/ajax/libs/twemoji/15.1.0/72x72/'
});
};
const getEmojiDatabase = option('emoticons_database');
const getEmojiDatabaseUrl = option('emoticons_database_url');
const getEmojiDatabaseId = option('emoticons_database_id');
const getAppendedEmoji = option('emoticons_append');
const getEmojiImageUrl = option('emoticons_images_url');
const ALL_CATEGORY = 'All';
const categoryNameMap = {
symbols: 'Symbols',
people: 'People',
animals_and_nature: 'Animals and Nature',
food_and_drink: 'Food and Drink',
activity: 'Activity',
travel_and_places: 'Travel and Places',
objects: 'Objects',
flags: 'Flags',
user: 'User Defined'
};
const translateCategory = (categories, name) => has(categories, name) ? categories[name] : name;
const getUserDefinedEmoji = (editor) => {
const userDefinedEmoticons = getAppendedEmoji(editor);
return map(userDefinedEmoticons, (value) =>
// Set some sane defaults for the custom emoji entry
({ keywords: [], category: 'user', ...value }));
};
// TODO: Consider how to share this loading across different editors
const initDatabase = (editor, databaseUrl, databaseId) => {
const categories = value();
const all = value();
const emojiImagesUrl = getEmojiImageUrl(editor);
const getEmoji = (lib) => {
// Note: This is a little hacky, but the database doesn't provide a way for us to tell what sort of database is being used
if (startsWith(lib.char, '<img')) {
return lib.char.replace(/src="([^"]+)"/, (match, url) => `src="${emojiImagesUrl}${url}"`);
}
else {
return lib.char;
}
};
const processEmojis = (emojis) => {
const cats = {};
const everything = [];
each(emojis, (lib, title) => {
const entry = {
// Omitting fitzpatrick_scale
title,
keywords: lib.keywords,
char: getEmoji(lib),
category: translateCategory(categoryNameMap, lib.category)
};
const current = cats[entry.category] !== undefined ? cats[entry.category] : [];
cats[entry.category] = current.concat([entry]);
everything.push(entry);
});
categories.set(cats);
all.set(everything);
};
editor.on('init', () => {
global.load(databaseId, databaseUrl).then((emojis) => {
const userEmojis = getUserDefinedEmoji(editor);
processEmojis(merge(emojis, userEmojis));
}, (err) => {
// eslint-disable-next-line no-console
console.log(`Failed to load emojis: ${err}`);
categories.set({});
all.set([]);
});
});
const listCategory = (category) => {
if (category === ALL_CATEGORY) {
return listAll();
}
return categories.get().bind((cats) => Optional.from(cats[category])).getOr([]);
};
const listAll = () => all.get().getOr([]);
const listCategories = () =>
// TODO: Category key order should be adjusted to match the standard
[ALL_CATEGORY].concat(keys(categories.get().getOr({})));
const waitForLoad = () => {
if (hasLoaded()) {
return Promise.resolve(true);
}
else {
return new Promise((resolve, reject) => {
let numRetries = 15;
const interval = setInterval(() => {
if (hasLoaded()) {
clearInterval(interval);
resolve(true);
}
else {
numRetries--;
if (numRetries < 0) {
// eslint-disable-next-line no-console
console.log('Could not load emojis from url: ' + databaseUrl);
clearInterval(interval);
reject(false);
}
}
}, 100);
});
}
};
const hasLoaded = () => categories.isSet() && all.isSet();
return {
listCategories,
hasLoaded,
waitForLoad,
listAll,
listCategory
};
};
const emojiMatches = (emoji, lowerCasePattern) => contains(emoji.title.toLowerCase(), lowerCasePattern) ||
exists(emoji.keywords, (k) => contains(k.toLowerCase(), lowerCasePattern));
const emojisFrom = (list, pattern, maxResults) => {
const matches = [];
const lowerCasePattern = pattern.toLowerCase();
const reachedLimit = maxResults.fold(() => never, (max) => (size) => size >= max);
for (let i = 0; i < list.length; i++) {
// TODO: more intelligent search by showing title matches at the top, keyword matches after that (use two arrays and concat at the end)
if (pattern.length === 0 || emojiMatches(list[i], lowerCasePattern)) {
matches.push({
value: list[i].char,
text: list[i].title,
icon: list[i].char
});
if (reachedLimit(matches.length)) {
break;
}
}
}
return matches;
};
const patternName = 'pattern';
const open = (editor, database) => {
const initialState = {
pattern: '',
results: emojisFrom(database.listAll(), '', Optional.some(300))
};
const currentTab = Cell(ALL_CATEGORY);
const scan = (dialogApi) => {
const dialogData = dialogApi.getData();
const category = currentTab.get();
const candidates = database.listCategory(category);
const results = emojisFrom(candidates, dialogData[patternName], category === ALL_CATEGORY ? Optional.some(300) : Optional.none());
dialogApi.setData({
results
});
};
const updateFilter = last((dialogApi) => {
scan(dialogApi);
}, 200);
const searchField = {
label: 'Search',
type: 'input',
name: patternName
};
const resultsField = {
type: 'collection',
name: 'results'
// TODO TINY-3229 implement collection columns properly
// columns: 'auto'
};
const getInitialState = () => {
const body = {
type: 'tabpanel',
// All tabs have the same fields.
tabs: map$1(database.listCategories(), (cat) => ({
title: cat,
name: cat,
items: [searchField, resultsField]
}))
};
return {
title: 'Emojis',
size: 'normal',
body,
initialData: initialState,
onTabChange: (dialogApi, details) => {
currentTab.set(details.newTabName);
updateFilter.throttle(dialogApi);
},
onChange: updateFilter.throttle,
onAction: (dialogApi, actionData) => {
if (actionData.name === 'results') {
insertEmoticon(editor, actionData.value);
dialogApi.close();
}
},
buttons: [
{
type: 'cancel',
text: 'Close',
primary: true
}
]
};
};
const dialogApi = editor.windowManager.open(getInitialState());
dialogApi.focus(patternName);
if (!database.hasLoaded()) {
dialogApi.block('Loading emojis...');
database.waitForLoad().then(() => {
dialogApi.redial(getInitialState());
updateFilter.throttle(dialogApi);
dialogApi.focus(patternName);
dialogApi.unblock();
}).catch((_err) => {
dialogApi.redial({
title: 'Emojis',
body: {
type: 'panel',
items: [
{
type: 'alertbanner',
level: 'error',
icon: 'warning',
text: 'Could not load emojis'
}
]
},
buttons: [
{
type: 'cancel',
text: 'Close',
primary: true
}
],
initialData: {
pattern: '',
results: []
}
});
dialogApi.focus(patternName);
dialogApi.unblock();
});
}
};
const register$1 = (editor, database) => {
editor.addCommand('mceEmoticons', () => open(editor, database));
};
const setup = (editor) => {
editor.on('PreInit', () => {
editor.parser.addAttributeFilter('data-emoticon', (nodes) => {
each$1(nodes, (node) => {
node.attr('data-mce-resize', 'false');
node.attr('data-mce-placeholder', '1');
});
});
});
};
const init = (editor, database) => {
editor.ui.registry.addAutocompleter('emoticons', {
trigger: ':',
columns: 'auto',
minChars: 2,
fetch: (pattern, maxResults) => database.waitForLoad().then(() => {
const candidates = database.listAll();
return emojisFrom(candidates, pattern, Optional.some(maxResults));
}),
onAction: (autocompleteApi, rng, value) => {
editor.selection.setRng(rng);
editor.insertContent(value);
autocompleteApi.hide();
}
});
};
const onSetupEditable = (editor) => (api) => {
const nodeChanged = () => {
api.setEnabled(editor.selection.isEditable());
};
editor.on('NodeChange', nodeChanged);
nodeChanged();
return () => {
editor.off('NodeChange', nodeChanged);
};
};
const register = (editor) => {
const onAction = () => editor.execCommand('mceEmoticons');
editor.ui.registry.addButton('emoticons', {
tooltip: 'Emojis',
icon: 'emoji',
onAction,
onSetup: onSetupEditable(editor)
});
editor.ui.registry.addMenuItem('emoticons', {
text: 'Emojis...',
icon: 'emoji',
onAction,
onSetup: onSetupEditable(editor)
});
};
/**
* This class contains all core logic for the emoticons plugin.
*
* @class tinymce.emoticons.Plugin
* @private
*/
var Plugin = () => {
global$1.add('emoticons', (editor, pluginUrl) => {
register$2(editor, pluginUrl);
const databaseUrl = getEmojiDatabaseUrl(editor);
const databaseId = getEmojiDatabaseId(editor);
const database = initDatabase(editor, databaseUrl, databaseId);
register$1(editor, database);
register(editor);
init(editor, database);
setup(editor);
return {
getAllEmojis: () => database.waitForLoad().then(() => database.listAll())
};
});
};
Plugin();
/** *****
* DO NOT EXPORT ANYTHING
*
* IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE
*******/
})();
File diff suppressed because one or more lines are too long
-7
View File
@@ -1,7 +0,0 @@
// Exports the "fullscreen" plugin for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/plugins/fullscreen')
// ES2015:
// import 'tinymce/plugins/fullscreen'
require('./plugin.js');
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-7
View File
@@ -1,7 +0,0 @@
// Exports the "help" plugin for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/plugins/help')
// ES2015:
// import 'tinymce/plugins/help'
require('./plugin.js');
@@ -1,5 +1,5 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.ar',
'<h1>بدء التنقل بواسطة لوحة المفاتيح</h1>\n' +
'<h1>بدء التنقل بواسطة لوحة المفاتيح</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>التركيز على شريط القوائم</dt>\n' +
@@ -1,93 +0,0 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.bg-BG',
'<h1>Начало на навигацията с клавиатурата</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Фокусиране върху лентата с менюта</dt>\n' +
' <dd>Windows или Linux: Alt+F9</dd>\n' +
' <dd>macOS: &#x2325;F9</dd>\n' +
' <dt>Фокусиране върху лентата с инструменти</dt>\n' +
' <dd>Windows или Linux: Alt+F10</dd>\n' +
' <dd>macOS: &#x2325;F10</dd>\n' +
' <dt>Фокусиране върху долния колонтитул</dt>\n' +
' <dd>Windows или Linux: Alt+F11</dd>\n' +
' <dd>macOS: &#x2325;F11</dd>\n' +
' <dt>Фокусиране на известието</dt>\n' +
' <dd>Windows или Linux: Alt+F12</dd>\n' +
' <dd>macOS: &#x2325;F12</dd>\n' +
' <dt>Фокусиране върху контекстуалната лента с инструменти</dt>\n' +
' <dd>Windows, Linux или macOS: Ctrl+F9</dd>\n' +
'</dl>\n' +
'\n' +
'<p>Навигацията ще започне с първия елемент на ПИ, който ще бъде маркиран или подчертан в случая на първия елемент в\n' +
' пътя до елемента в долния колонтитул.</p>\n' +
'\n' +
'<h1>Навигиране между раздели на ПИ</h1>\n' +
'\n' +
'<p>За да преминете от един раздел на ПИ към следващия, натиснете <strong>Tab</strong>.</p>\n' +
'\n' +
'<p>За да преминете от един раздел на ПИ към предишния, натиснете <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<p>Редът за <strong>обхождане с табулация</strong> на тези раздели на ПИ е:</p>\n' +
'\n' +
'<ol>\n' +
' <li>Лентата с менюта</li>\n' +
' <li>Всяка група на лентата с инструменти</li>\n' +
' <li>Страничната лента</li>\n' +
' <li>Пътят до елемента в долния колонтитул</li>\n' +
' <li>Бутонът за превключване на броя на думите в долния колонтитул</li>\n' +
' <li>Връзката за търговска марка в долния колонтитул</li>\n' +
' <li>Манипулаторът за преоразмеряване на редактора в долния колонтитул</li>\n' +
'</ol>\n' +
'\n' +
'<p>Ако някой раздел на ПИ липсва, той се пропуска.</p>\n' +
'\n' +
'<p>Ако долният колонтитул има фокус за навигация с клавиатурата и няма странична лента, натискането на <strong>Shift+Tab</strong>\n' +
' премества фокуса към първата група на лентата с инструменти, а не към последната.</p>\n' +
'\n' +
'<h1>Навигиране в разделите на ПИ</h1>\n' +
'\n' +
'<p>За да преминете от един елемент на ПИ към следващия, натиснете съответния клавиш със <strong>стрелка</strong>.</p>\n' +
'\n' +
'<p>С клавишите със стрелка <strong>наляво</strong> и <strong>надясно</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>се придвижвате между менютата в лентата с менюто;</li>\n' +
' <li>отваряте подменю в меню;</li>\n' +
' <li>се придвижвате между бутоните в група на лентата с инструменти;</li>\n' +
' <li>се придвижвате между елементи в пътя до елемент в долния колонтитул.</li>\n' +
'</ul>\n' +
'\n' +
'<p>С клавишите със стрелка <strong>надолу</strong> и <strong>нагоре</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>се придвижвате между елементите от менюто в дадено меню;</li>\n' +
' <li>се придвижвате между елементите в изскачащо меню на лентата с инструменти.</li>\n' +
'</ul>\n' +
'\n' +
'<p>Клавишите със <strong>стрелки</strong> се придвижват в рамките на фокусирания раздел на ПИ.</p>\n' +
'\n' +
'<p>За да затворите отворено меню, подменю или изскачащо меню, натиснете клавиша <strong>Esc</strong>.</p>\n' +
'\n' +
'<p>Ако текущият фокус е върху „горната част“ на конкретен раздел на ПИ, натискането на клавиша <strong>Esc</strong> също излиза\n' +
' напълно от навигацията с клавиатурата.</p>\n' +
'\n' +
'<h1>Изпълнение на елемент от менюто или бутон от лентата с инструменти</h1>\n' +
'\n' +
'<p>Когато желаният елемент от менюто или бутон от лентата с инструменти е маркиран, натиснете <strong>Return</strong>, <strong>Enter</strong>\n' +
' или <strong>клавиша за интервал</strong>, за да изпълните елемента.</p>\n' +
'\n' +
'<h1>Навигиране в диалогови прозорци без раздели</h1>\n' +
'\n' +
'<p>В диалоговите прозорци без раздели първият интерактивен компонент се фокусира, когато се отвори диалоговият прозорец.</p>\n' +
'\n' +
'<p>Навигирайте между интерактивните компоненти на диалоговия прозорец, като натиснете <strong>Tab</strong> или <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<h1>Навигиране в диалогови прозорци с раздели</h1>\n' +
'\n' +
'<p>В диалоговите прозорци с раздели първият бутон в менюто с раздели се фокусира, когато се отвори диалоговият прозорец.</p>\n' +
'\n' +
'<p>Навигирайте между интерактивните компоненти на този диалогов раздел, като натиснете <strong>Tab</strong> или\n' +
' <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<p>Превключете към друг диалогов раздел, като фокусирате върху менюто с раздели и след това натиснете съответния клавиш със <strong>стрелка</strong>,\n' +
' за да преминете през наличните раздели.</p>\n');
@@ -1,5 +1,5 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.bg_BG',
'<h1>Начало на навигацията с клавиатурата</h1>\n' +
'<h1>Начало на навигацията с клавиатурата</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Фокусиране върху лентата с менюта</dt>\n' +
@@ -1,5 +1,5 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.ca',
'<h1>Inici de la navegació amb el teclat</h1>\n' +
'<h1>Inici de la navegació amb el teclat</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Enfocar la barra de menús</dt>\n' +
@@ -1,5 +1,5 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.cs',
'<h1>Začínáme navigovat pomocí klávesnice</h1>\n' +
'<h1>Začínáme navigovat pomocí klávesnice</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Přejít na řádek nabídek</dt>\n' +
@@ -1,5 +1,5 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.da',
'<h1>Start tastaturnavigation</h1>\n' +
'<h1>Start tastaturnavigation</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Fokuser på menulinjen</dt>\n' +
@@ -1,5 +1,5 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.de',
'<h1>Grundlagen der Tastaturnavigation</h1>\n' +
'<h1>Grundlagen der Tastaturnavigation</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Fokus auf Menüleiste</dt>\n' +
@@ -1,5 +1,5 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.el',
'<h1>Έναρξη πλοήγησης μέσω πληκτρολογίου</h1>\n' +
'<h1>Έναρξη πλοήγησης μέσω πληκτρολογίου</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Εστίαση στη γραμμή μενού</dt>\n' +
@@ -1,5 +1,5 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.en',
'<h1>Begin keyboard navigation</h1>\n' +
'<h1>Begin keyboard navigation</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Focus the Menu bar</dt>\n' +
@@ -1,5 +1,5 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.es',
'<h1>Iniciar la navegación con el teclado</h1>\n' +
'<h1>Iniciar la navegación con el teclado</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Enfocar la barra de menús</dt>\n' +
@@ -1,5 +1,5 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.eu',
'<h1>Hasi teklatuaren nabigazioa</h1>\n' +
'<h1>Hasi teklatuaren nabigazioa</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Fokuratu menu-barra</dt>\n' +
@@ -1,5 +1,5 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.fa',
'<h1>شروع پیمایش صفحه‌کلید</h1>\n' +
'<h1>شروع پیمایش صفحه‌کلید</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>تمرکز بر نوار منو</dt>\n' +
@@ -1,5 +1,5 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.fi',
'<h1>Näppäimistönavigoinnin aloittaminen</h1>\n' +
'<h1>Näppäimistönavigoinnin aloittaminen</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Siirrä kohdistus valikkopalkkiin</dt>\n' +
@@ -1,93 +0,0 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.fr-FR',
'<h1>Débuter la navigation au clavier</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Cibler la barre du menu</dt>\n' +
' <dd>Windows ou Linux : Alt+F9</dd>\n' +
' <dd>macOS : &#x2325;F9</dd>\n' +
" <dt>Cibler la barre d'outils</dt>\n" +
' <dd>Windows ou Linux : Alt+F10</dd>\n' +
' <dd>macOS : &#x2325;F10</dd>\n' +
' <dt>Cibler le pied de page</dt>\n' +
' <dd>Windows ou Linux : Alt+F11</dd>\n' +
' <dd>macOS : &#x2325;F11</dd>\n' +
' <dt>Cibler la notification</dt>\n' +
' <dd>Windows ou Linux : Alt+F12</dd>\n' +
' <dd>macOS : &#x2325;F12</dd>\n' +
" <dt>Cibler une barre d'outils contextuelle</dt>\n" +
' <dd>Windows, Linux ou macOS : Ctrl+F9</dd>\n' +
'</dl>\n' +
'\n' +
"<p>La navigation débutera sur le premier élément de l'interface utilisateur, qui sera mis en surbrillance ou bien souligné dans le cas du premier élément du\n" +
" chemin d'éléments du pied de page.</p>\n" +
'\n' +
"<h1>Naviguer entre les sections de l'interface utilisateur</h1>\n" +
'\n' +
"<p>Pour passer d'une section de l'interface utilisateur à la suivante, appuyez sur <strong>Tabulation</strong>.</p>\n" +
'\n' +
"<p>Pour passer d'une section de l'interface utilisateur à la précédente, appuyez sur <strong>Maj+Tabulation</strong>.</p>\n" +
'\n' +
"<p>L'ordre de <strong>Tabulation</strong> de ces sections de l'interface utilisateur est le suivant :</p>\n" +
'\n' +
'<ol>\n' +
' <li>Barre du menu</li>\n' +
" <li>Chaque groupe de barres d'outils</li>\n" +
' <li>Barre latérale</li>\n' +
" <li>Chemin d'éléments du pied de page</li>\n" +
" <li>Bouton d'activation du compteur de mots dans le pied de page</li>\n" +
' <li>Lien de marque dans le pied de page</li>\n' +
" <li>Poignée de redimensionnement de l'éditeur dans le pied de page</li>\n" +
'</ol>\n' +
'\n' +
"<p>Si une section de l'interface utilisateur n'est pas présente, elle sera ignorée.</p>\n" +
'\n' +
"<p>Si le pied de page comporte un ciblage par navigation au clavier et qu'il n'y a aucune barre latérale visible, appuyer sur <strong>Maj+Tabulation</strong>\n" +
" déplace le ciblage vers le premier groupe de barres d'outils et non le dernier.</p>\n" +
'\n' +
"<h1>Naviguer au sein des sections de l'interface utilisateur</h1>\n" +
'\n' +
"<p>Pour passer d'un élément de l'interface utilisateur au suivant, appuyez sur la <strong>Flèche</strong> appropriée.</p>\n" +
'\n' +
'<p>Les touches fléchées <strong>Gauche</strong> et <strong>Droite</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>se déplacent entre les menus de la barre des menus.</li>\n' +
" <li>ouvrent un sous-menu au sein d'un menu.</li>\n" +
" <li>se déplacent entre les boutons d'un groupe de barres d'outils.</li>\n" +
" <li>se déplacent entre les éléments du chemin d'éléments du pied de page.</li>\n" +
'</ul>\n' +
'\n' +
'<p>Les touches fléchées <strong>Bas</strong> et <strong>Haut</strong></p>\n' +
'\n' +
'<ul>\n' +
" <li>se déplacent entre les éléments de menu au sein d'un menu.</li>\n" +
" <li>se déplacent entre les éléments au sein d'un menu contextuel de barre d'outils.</li>\n" +
'</ul>\n' +
'\n' +
"<p>Les <strong>Flèches</strong> parcourent la section de l'interface utilisateur ciblée.</p>\n" +
'\n' +
'<p>Pour fermer un menu ouvert, un sous-menu ouvert ou un menu contextuel ouvert, appuyez sur <strong>Echap</strong>.</p>\n' +
'\n' +
"<p>Si l'actuel ciblage se trouve en « haut » d'une section spécifique de l'interface utilisateur, appuyer sur <strong>Echap</strong> permet également de quitter\n" +
' entièrement la navigation au clavier.</p>\n' +
'\n' +
"<h1>Exécuter un élément de menu ou un bouton de barre d'outils</h1>\n" +
'\n' +
"<p>Lorsque l'élément de menu ou le bouton de barre d'outils désiré est mis en surbrillance, appuyez sur la touche <strong>Retour arrière</strong>, <strong>Entrée</strong>\n" +
" ou la <strong>Barre d'espace</strong> pour exécuter l'élément.</p>\n" +
'\n' +
'<h1>Naviguer au sein de dialogues sans onglets</h1>\n' +
'\n' +
"<p>Dans les dialogues sans onglets, le premier composant interactif est ciblé lorsque le dialogue s'ouvre.</p>\n" +
'\n' +
'<p>Naviguez entre les composants du dialogue interactif en appuyant sur <strong>Tabulation</strong> ou <strong>Maj+Tabulation</strong>.</p>\n' +
'\n' +
'<h1>Naviguer au sein de dialogues avec onglets</h1>\n' +
'\n' +
"<p>Dans les dialogues avec onglets, le premier bouton du menu de l'onglet est ciblé lorsque le dialogue s'ouvre.</p>\n" +
'\n' +
'<p>Naviguez entre les composants interactifs de cet onglet de dialogue en appuyant sur <strong>Tabulation</strong> ou\n' +
' <strong>Maj+Tabulation</strong>.</p>\n' +
'\n' +
"<p>Passez à un autre onglet de dialogue en ciblant le menu de l'onglet et en appuyant sur la <strong>Flèche</strong>\n" +
' appropriée pour parcourir les onglets disponibles.</p>\n');
@@ -1,5 +1,5 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.fr_FR',
'<h1>Débuter la navigation au clavier</h1>\n' +
'<h1>Débuter la navigation au clavier</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Cibler la barre du menu</dt>\n' +

Some files were not shown because too many files have changed in this diff Show More