Compare commits
42 Commits
hornbeam_g
...
main
Author | SHA1 | Date | |
---|---|---|---|
9585ec5a51 | |||
98de78aa72 | |||
02db5531d3 | |||
1cb196571f | |||
2281a9bb50 | |||
9098d72217 | |||
7cb653ab67 | |||
5a115b3a20 | |||
659f3a88e4 | |||
16f3488d20 | |||
dc8447f08e | |||
845a05b720 | |||
d08a088257 | |||
c92ab5aaff | |||
d9f008d5ed | |||
d08c04de5b | |||
708f5d214d | |||
75121c2fb8 | |||
f21500d5e8 | |||
b69662cfdc | |||
5448888651 | |||
8a3cc0eeef | |||
b6181eb204 | |||
dd74768ec6 | |||
cdc34d45f7 | |||
b7818dddc1 | |||
981812c796 | |||
f29573d9ad | |||
00e67d34d9 | |||
b119bf376c | |||
91ba94a38a | |||
7171145aa5 | |||
f638ec66b4 | |||
407a6b267e | |||
11316a9cfc | |||
8fa06fc77e | |||
5e33d97dfa | |||
5fc8467966 | |||
4c27187926 | |||
159457616a | |||
cbfdece2cb | |||
3b369200aa |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
/.idea
|
/.idea
|
||||||
|
/.devenv
|
||||||
|
/.direnv
|
||||||
|
603
Cargo.lock
generated
603
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -5,10 +5,12 @@ members = [
|
|||||||
"hornbeam_interpreter",
|
"hornbeam_interpreter",
|
||||||
"hornbeam_macros",
|
"hornbeam_macros",
|
||||||
"hornbeam",
|
"hornbeam",
|
||||||
"demo_hornbeam_project",
|
"demo_hornbeam_project", "formbeam", "formbeam_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
bevy_reflect = "0.14.0"
|
||||||
|
|
||||||
# Enable optimisation for testing helpers
|
# Enable optimisation for testing helpers
|
||||||
[profile.dev.package.insta]
|
[profile.dev.package.insta]
|
||||||
|
661
LICENCE.txt
Normal file
661
LICENCE.txt
Normal file
@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 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 Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
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 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 work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero 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 Affero 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 Affero 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 Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero 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 your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
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 AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
17
README.md
17
README.md
@ -12,5 +12,20 @@ WIP. Will be: a lightweight and nimble HTML templating engine, designed for comp
|
|||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
TODO. Currently All Rights Reserved.
|
Currently under the AGPL 3 or later, but this is relatively likely to be changed at a later date.
|
||||||
|
|
||||||
|
See `LICENCE.txt`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Releasing
|
||||||
|
|
||||||
|
This is the command used to cut a release:
|
||||||
|
|
||||||
|
```shell-commands
|
||||||
|
cargo ws publish patch --all --force '*'
|
||||||
|
```
|
||||||
|
|
||||||
|
* `patch` could be `major` or `minor` instead.
|
||||||
|
* `--force '*'` means all packages are bumped, even though they have no changes. This keeps the version numbers in sync.
|
||||||
|
* `--all` means `demo_hornbeam_project` will be bumped even though it is not published.
|
||||||
|
@ -1,15 +1,22 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "demo_hornbeam_project"
|
name = "demo_hornbeam_project"
|
||||||
version = "0.0.1"
|
version = "0.0.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
private = true
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.6.9"
|
axum = "0.8.4"
|
||||||
|
bevy_reflect.workspace = true
|
||||||
eyre = "0.6.8"
|
eyre = "0.6.8"
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
tokio = { version = "1.25.0", features = ["full"] }
|
tokio = { version = "1.25.0", features = ["full"] }
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
hornbeam = { version = "0.0.1", path = "../hornbeam" }
|
hornbeam = { version = "0.0.5", path = "../hornbeam", features = ["formbeam"] }
|
||||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||||
|
|
||||||
|
async-trait = "0.1.81"
|
||||||
|
formbeam = { version = "0.0.5", path = "../formbeam" }
|
||||||
|
formbeam_derive = { version = "0.0.5", path = "../formbeam_derive" }
|
||||||
|
serde = { version = "1.0.204", features = ["derive"] }
|
||||||
|
@ -1,14 +1,23 @@
|
|||||||
use axum::extract::Path;
|
use axum::extract::rejection::FormRejection;
|
||||||
|
use axum::extract::{self, Path};
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::{Html, IntoResponse, Response};
|
use axum::response::{Html, IntoResponse, Response};
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use hornbeam::{initialise_template_manager, make_template_manager, render_template_string};
|
use eyre::Context;
|
||||||
|
use formbeam::traits::FormValidation;
|
||||||
|
use formbeam::FormPartial;
|
||||||
|
use hornbeam::{
|
||||||
|
initialise_template_manager, make_template_manager, render_template_string, ReflectedForm,
|
||||||
|
};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
|
||||||
|
use formbeam_derive::Form;
|
||||||
|
|
||||||
make_template_manager! {
|
make_template_manager! {
|
||||||
static ref TEMPLATING = {
|
static ref TEMPLATING = {
|
||||||
default_locale: "en",
|
default_locale: "en",
|
||||||
@ -28,13 +37,16 @@ async fn main() -> eyre::Result<()> {
|
|||||||
|
|
||||||
initialise_template_manager!(TEMPLATING);
|
initialise_template_manager!(TEMPLATING);
|
||||||
|
|
||||||
let app = Router::new().route("/:lang/hello/:name", get(say_hello));
|
let app = Router::new()
|
||||||
|
.route("/:lang/hello/:name", get(say_hello))
|
||||||
|
.route("/:lang/formdemo", get(form_demo));
|
||||||
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
|
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||||
|
let listener = TcpListener::bind(addr)
|
||||||
|
.await
|
||||||
|
.context("Failed to listen on :8080")?;
|
||||||
debug!("Listening on http://{}", addr);
|
debug!("Listening on http://{}", addr);
|
||||||
axum::Server::bind(&addr)
|
axum::serve(listener, app).await?;
|
||||||
.serve(app.into_make_service())
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,3 +72,38 @@ impl IntoResponse for Rendered {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Form)]
|
||||||
|
pub struct MyForm {
|
||||||
|
#[form(min_chars(2), max_chars(10), regex(r"^[^_:]+$"))]
|
||||||
|
name: String,
|
||||||
|
#[form(email)]
|
||||||
|
email: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn form_demo(
|
||||||
|
Path(lang): Path<String>,
|
||||||
|
form_in: Result<extract::Form<MyFormRaw>, FormRejection>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let (form, valid_submission) = match form_in {
|
||||||
|
Ok(extract::Form(form_raw)) => {
|
||||||
|
let validation = form_raw.validate().await.unwrap();
|
||||||
|
|
||||||
|
if validation.is_valid() {
|
||||||
|
let submitted = form_raw.form().unwrap();
|
||||||
|
let MyForm { name, email } = submitted;
|
||||||
|
(
|
||||||
|
Default::default(),
|
||||||
|
Some(format!("hello {name} ({email:?})")),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(ReflectedForm::new(form_raw, validation), None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => (Default::default(), None),
|
||||||
|
};
|
||||||
|
|
||||||
|
Rendered(render_template_string!(TEMPLATING, form_demo, lang, {
|
||||||
|
form, valid_submission
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
if $errors.len() != 0
|
||||||
|
"errors on this field (or 'form-wide' section):"
|
||||||
|
ul
|
||||||
|
for $e in $errors
|
||||||
|
li
|
||||||
|
"${$e.error_code()}"
|
@ -0,0 +1,19 @@
|
|||||||
|
"validators on this field:"
|
||||||
|
ul
|
||||||
|
for $fv in $form.info.field_validators($name)
|
||||||
|
li
|
||||||
|
match $fv
|
||||||
|
MinLength($l) =>
|
||||||
|
"minimum length: $l"
|
||||||
|
MaxLength($l) =>
|
||||||
|
"maximum length: $l"
|
||||||
|
Required =>
|
||||||
|
"this field is required"
|
||||||
|
Email =>
|
||||||
|
"e-mail address"
|
||||||
|
Regex($pat) =>
|
||||||
|
"matches regex: $pat"
|
||||||
|
Custom($cust) =>
|
||||||
|
"(custom) $cust"
|
||||||
|
_ =>
|
||||||
|
"(other)"
|
48
demo_hornbeam_project/templates/pages/form_demo.hnb
Normal file
48
demo_hornbeam_project/templates/pages/form_demo.hnb
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
html
|
||||||
|
head
|
||||||
|
title
|
||||||
|
"Hello!"
|
||||||
|
|
||||||
|
body
|
||||||
|
h1
|
||||||
|
"Form demo!"
|
||||||
|
|
||||||
|
|
||||||
|
match $valid_submission
|
||||||
|
Some($vs) =>
|
||||||
|
"great, you managed to submit a form! $vs"
|
||||||
|
br
|
||||||
|
"You can send another one if you want :)."
|
||||||
|
|
||||||
|
None =>
|
||||||
|
"Why don't you try submitting a form?"
|
||||||
|
|
||||||
|
hr
|
||||||
|
|
||||||
|
form {method="GET", action=""}
|
||||||
|
ShowErrors {errors=$form.errors.form_wide}
|
||||||
|
|
||||||
|
b
|
||||||
|
"Name"
|
||||||
|
input {type="text", name="name", value=$form.raw.name.unwrap_or("")}
|
||||||
|
|
||||||
|
br
|
||||||
|
ShowValidators {$form, name="name"}
|
||||||
|
br
|
||||||
|
ShowErrors {errors=$form.errors.name}
|
||||||
|
|
||||||
|
hr
|
||||||
|
|
||||||
|
b
|
||||||
|
"E-mail address"
|
||||||
|
input {type="email", name="email", value=$form.raw.email.unwrap_or("")}
|
||||||
|
|
||||||
|
br
|
||||||
|
ShowValidators {$form, name="email"}
|
||||||
|
br
|
||||||
|
ShowErrors {errors=$form.errors.email}
|
||||||
|
|
||||||
|
hr
|
||||||
|
|
||||||
|
button {type="submit"}
|
||||||
|
"Submit!"
|
291
flake.lock
generated
Normal file
291
flake.lock
generated
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"devenv": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"nix": "nix",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"pre-commit-hooks": "pre-commit-hooks"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1688058187,
|
||||||
|
"narHash": "sha256-ipDcc7qrucpJ0+0eYNlwnE+ISTcq4m03qW+CWUshRXI=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "devenv",
|
||||||
|
"rev": "c8778e3dc30eb9043e218aaa3861d42d4992de77",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"ref": "v0.6.3",
|
||||||
|
"repo": "devenv",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fenix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1752907304,
|
||||||
|
"narHash": "sha256-rSw0b/ahoZebcp+AZG7uoScB5Q59TYEE5Kx8k0pZp9E=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "e91719882d0e4366202cc9058eb21df74c0bdb92",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1673956053,
|
||||||
|
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1667395993,
|
||||||
|
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitignore": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"pre-commit-hooks",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1660459072,
|
||||||
|
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lowdown-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1633514407,
|
||||||
|
"narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=",
|
||||||
|
"owner": "kristapsdz",
|
||||||
|
"repo": "lowdown",
|
||||||
|
"rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "kristapsdz",
|
||||||
|
"repo": "lowdown",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nix": {
|
||||||
|
"inputs": {
|
||||||
|
"lowdown-src": "lowdown-src",
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"nixpkgs-regression": "nixpkgs-regression"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1676545802,
|
||||||
|
"narHash": "sha256-EK4rZ+Hd5hsvXnzSzk2ikhStJnD63odF7SzsQ8CuSPU=",
|
||||||
|
"owner": "domenkozar",
|
||||||
|
"repo": "nix",
|
||||||
|
"rev": "7c91803598ffbcfe4a55c44ac6d49b2cf07a527f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "domenkozar",
|
||||||
|
"ref": "relaxed-flakes",
|
||||||
|
"repo": "nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1678875422,
|
||||||
|
"narHash": "sha256-T3o6NcQPwXjxJMn2shz86Chch4ljXgZn746c2caGxd8=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "126f49a01de5b7e35a43fd43f891ecf6d3a51459",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-regression": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1643052045,
|
||||||
|
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-stable": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1678872516,
|
||||||
|
"narHash": "sha256-/E1YwtMtFAu2KUQKV/1+KFuReYPANM2Rzehk84VxVoc=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "9b8e5abb18324c7fe9f07cb100c3cd4a29cda8b8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-22.11",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1752620740,
|
||||||
|
"narHash": "sha256-f3pO+9lg66mV7IMmmIqG4PL3223TYMlnlw+pnpelbss=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "32a4e87942101f1c9f9865e04dc3ddb175f5f32e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"ref": "nixos-25.05",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pre-commit-hooks": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": [
|
||||||
|
"devenv",
|
||||||
|
"flake-compat"
|
||||||
|
],
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"gitignore": "gitignore",
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"nixpkgs-stable": "nixpkgs-stable"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1686050334,
|
||||||
|
"narHash": "sha256-R0mczWjDzBpIvM3XXhO908X5e2CQqjyh/gFbwZk/7/Q=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "pre-commit-hooks.nix",
|
||||||
|
"rev": "6881eb2ae5d8a3516e34714e7a90d9d95914c4dc",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "pre-commit-hooks.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"devenv": "devenv",
|
||||||
|
"fenix": "fenix",
|
||||||
|
"nixpkgs": "nixpkgs_2",
|
||||||
|
"utils": "utils"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-analyzer-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1752817855,
|
||||||
|
"narHash": "sha256-YnG3d44oX+g2ooUsNWT+Ii24w6T+b0dj86k0HkIFUj4=",
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"rev": "330c4ed11c4e1eef0999a2cd629703a601da1436",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"ref": "nightly",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1710146030,
|
||||||
|
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
84
flake.nix
Normal file
84
flake.nix
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
description = "Hornbeam";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
utils.url = "github:numtide/flake-utils";
|
||||||
|
# Current Rust in nixpkgs is too old unfortunately — let's use the Fenix overlay's packages...
|
||||||
|
fenix = {
|
||||||
|
url = "github:nix-community/fenix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
nixpkgs.url = "nixpkgs/nixos-25.05";
|
||||||
|
|
||||||
|
devenv.url = "github:cachix/devenv/v0.6.3";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = inputs @ { self, nixpkgs, utils, fenix, devenv }:
|
||||||
|
utils.lib.eachDefaultSystem (system: let
|
||||||
|
pkgs = nixpkgs.legacyPackages."${system}";
|
||||||
|
|
||||||
|
fenixRustToolchain =
|
||||||
|
fenix.packages."${system}".stable.withComponents [
|
||||||
|
"cargo"
|
||||||
|
"clippy"
|
||||||
|
"rust-src"
|
||||||
|
"rustc"
|
||||||
|
"rustfmt"
|
||||||
|
"rust-analyzer"
|
||||||
|
];
|
||||||
|
in rec {
|
||||||
|
# `nix develop`
|
||||||
|
devShell = devenv.lib.mkShell {
|
||||||
|
inherit inputs pkgs;
|
||||||
|
modules = [
|
||||||
|
{
|
||||||
|
# Configure packages to install.
|
||||||
|
# Search for package names at https://search.nixos.org/packages?channel=unstable
|
||||||
|
packages = [
|
||||||
|
fenixRustToolchain
|
||||||
|
pkgs.gcc
|
||||||
|
|
||||||
|
# Snapshot testing
|
||||||
|
pkgs.cargo-insta
|
||||||
|
|
||||||
|
# Releasing a full workspace of packages
|
||||||
|
pkgs.cargo-workspaces
|
||||||
|
|
||||||
|
# Macro debugging
|
||||||
|
pkgs.cargo-expand
|
||||||
|
|
||||||
|
pkgs.grass-sass
|
||||||
|
pkgs.entr
|
||||||
|
|
||||||
|
# TODO Future pkgs.mdbook
|
||||||
|
|
||||||
|
pkgs.pkg-config
|
||||||
|
];
|
||||||
|
|
||||||
|
env = {
|
||||||
|
# Needed for bindgen when binding to avahi
|
||||||
|
LIBCLANG_PATH="${pkgs.llvmPackages_latest.libclang.lib}/lib";
|
||||||
|
|
||||||
|
# Sometimes useful for reference.
|
||||||
|
RUST_SRC_PATH = "${fenixRustToolchain}/lib/rustlib/src/rust/library";
|
||||||
|
|
||||||
|
# Cargo culted:
|
||||||
|
# Add to rustc search path
|
||||||
|
#RUSTFLAGS = (builtins.map (a: ''-L ${a}/lib'') [
|
||||||
|
#]);
|
||||||
|
# Add to bindgen search path
|
||||||
|
BINDGEN_EXTRA_CLANG_ARGS =
|
||||||
|
# Includes with normal include path
|
||||||
|
(builtins.map (a: ''-I"${a}/include"'') [
|
||||||
|
# pkgs.glibc.dev
|
||||||
|
])
|
||||||
|
# Includes with special directory paths
|
||||||
|
++ [
|
||||||
|
# ''-I"${pkgs.llvmPackages_latest.libclang.lib}/lib/clang/${pkgs.llvmPackages_latest.libclang.version}/include"''
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
12
formbeam/Cargo.toml
Normal file
12
formbeam/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "formbeam"
|
||||||
|
description = "Form system for the Hornbeam template engine"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
version = "0.0.5"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait = "0.1.81"
|
||||||
|
regex = "1.10.5"
|
||||||
|
bevy_reflect.workspace = true
|
||||||
|
static_assertions = "1.1.0"
|
81
formbeam/src/errors.rs
Normal file
81
formbeam/src/errors.rs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
|
use bevy_reflect::{FromReflect, Reflect, TypePath};
|
||||||
|
use static_assertions::assert_impl_all;
|
||||||
|
|
||||||
|
#[derive(Clone, Reflect)]
|
||||||
|
// This makes the struct opaque to the reflection engine, meaning it will
|
||||||
|
// be cloned absolutely instead of being converted to a DynamicEnum.
|
||||||
|
// However it won't implement Enum. I still think that's preferable, so `error_code()` will work etc.`
|
||||||
|
#[reflect_value]
|
||||||
|
pub enum FieldError {
|
||||||
|
Missing,
|
||||||
|
|
||||||
|
TooShort {
|
||||||
|
current: u32,
|
||||||
|
min_length: u32,
|
||||||
|
max_length: u32,
|
||||||
|
unit: FieldUnit,
|
||||||
|
},
|
||||||
|
TooLong {
|
||||||
|
current: u32,
|
||||||
|
min_length: u32,
|
||||||
|
max_length: u32,
|
||||||
|
unit: FieldUnit,
|
||||||
|
},
|
||||||
|
|
||||||
|
Custom {
|
||||||
|
code: String,
|
||||||
|
description: String,
|
||||||
|
values: BTreeMap<String, String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FieldError {
|
||||||
|
pub fn error_code(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
FieldError::Missing => "missing",
|
||||||
|
FieldError::TooShort {
|
||||||
|
unit: FieldUnit::Characters,
|
||||||
|
..
|
||||||
|
} => "too_short_chars",
|
||||||
|
FieldError::TooLong {
|
||||||
|
unit: FieldUnit::Characters,
|
||||||
|
..
|
||||||
|
} => "too_long_chars",
|
||||||
|
FieldError::Custom { code, .. } => code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Reflect)]
|
||||||
|
pub enum FieldUnit {
|
||||||
|
Characters,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FieldUnit {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
FieldUnit::Characters => write!(f, "characters"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for FieldError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
FieldError::Missing => write!(f, "The field is missing"),
|
||||||
|
FieldError::TooShort { current, min_length, max_length, unit } => write!(f, "The field is too short ({current}); it should be between {min_length} and {max_length} {unit}."),
|
||||||
|
FieldError::TooLong { current, min_length, max_length, unit } => write!(f, "The field is too long ({current}); it should be between {min_length} and {max_length} {unit}."),
|
||||||
|
FieldError::Custom {
|
||||||
|
code,
|
||||||
|
description,
|
||||||
|
values,
|
||||||
|
} => write!(f, "[{code}] {description} {values:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type FieldErrors = Vec<FieldError>;
|
||||||
|
assert_impl_all!(FieldErrors: Reflect, FromReflect, TypePath);
|
9
formbeam/src/lib.rs
Normal file
9
formbeam/src/lib.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
pub mod errors;
|
||||||
|
pub mod traits;
|
||||||
|
pub mod validators;
|
||||||
|
|
||||||
|
pub use errors::{FieldError, FieldErrors};
|
||||||
|
pub use traits::{
|
||||||
|
FieldInfo, FieldValidator, FieldValidatorInfo, Form, FormPartial, FormPartialInfo,
|
||||||
|
FormValidator,
|
||||||
|
};
|
98
formbeam/src/traits.rs
Normal file
98
formbeam/src/traits.rs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::errors::FieldErrors;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait FormValidator<F: Form, C: Send + 'static> {
|
||||||
|
type Error: Send + 'static;
|
||||||
|
|
||||||
|
async fn validate(&self, form: &mut F, context: &mut C) -> Result<(), Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait FieldValidator<C: Send + 'static> {
|
||||||
|
type Error: Send + 'static;
|
||||||
|
|
||||||
|
async fn validate(
|
||||||
|
&self,
|
||||||
|
value: &str,
|
||||||
|
errors: &mut FieldErrors,
|
||||||
|
context: &mut C,
|
||||||
|
) -> Result<(), Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of a realised, fully validated and populated, form.
|
||||||
|
pub trait Form: Sized + 'static {
|
||||||
|
type Partial: FormPartial<Form = Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of a partially populated and as-yet-unvalidated form.
|
||||||
|
/// Structs for and implementations of this trait can be generated via derive macro.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait FormPartial {
|
||||||
|
type Form: Form<Partial = Self>;
|
||||||
|
type Validation: FormValidation<Partial = Self>;
|
||||||
|
type Error: Send + 'static;
|
||||||
|
|
||||||
|
/// Converts the partial into a form.
|
||||||
|
///
|
||||||
|
/// # Preconditions
|
||||||
|
///
|
||||||
|
/// - The form should already have been validated.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Only structural/type errors will be returned here, with only the name of the field
|
||||||
|
/// to be returned.
|
||||||
|
///
|
||||||
|
/// No other validation is performed here.
|
||||||
|
fn form(&self) -> Result<Self::Form, &'static str>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
|
||||||
|
/// Runs all the validators on the form and calculates errors.
|
||||||
|
///
|
||||||
|
/// Should only be called once.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns direct errors from validators if one was thrown.
|
||||||
|
async fn validate(&self) -> Result<Self::Validation, Self::Error>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
|
||||||
|
const INFO: &'static FormPartialInfo;
|
||||||
|
|
||||||
|
fn validator_info(&self) -> &'static FormPartialInfo {
|
||||||
|
Self::INFO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of validation.
|
||||||
|
pub trait FormValidation {
|
||||||
|
type Partial: FormPartial<Validation = Self>;
|
||||||
|
|
||||||
|
/// Returns true if the form is valid.
|
||||||
|
fn is_valid(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FormPartialInfo {
|
||||||
|
pub form_validators: &'static [&'static str],
|
||||||
|
pub fields: &'static [FieldInfo],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FieldInfo {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub validators: &'static [FieldValidatorInfo],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum FieldValidatorInfo {
|
||||||
|
MinLength(u32),
|
||||||
|
MaxLength(u32),
|
||||||
|
MinValue(i64),
|
||||||
|
MaxValue(i64),
|
||||||
|
Required,
|
||||||
|
Email,
|
||||||
|
Regex(&'static str),
|
||||||
|
Custom(&'static str),
|
||||||
|
}
|
102
formbeam/src/validators.rs
Normal file
102
formbeam/src/validators.rs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
use std::{collections::BTreeMap, convert::Infallible, sync::LazyLock};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
pub use regex::Regex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::{FieldError, FieldErrors, FieldUnit},
|
||||||
|
traits::FieldValidator,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Constrains the minimum and maximum length, counted in `char`s, of a text input.
|
||||||
|
pub struct LengthInChars {
|
||||||
|
pub min_length: u32,
|
||||||
|
pub max_length: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl FieldValidator<()> for LengthInChars {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
async fn validate(
|
||||||
|
&self,
|
||||||
|
value: &str,
|
||||||
|
errors: &mut FieldErrors,
|
||||||
|
_context: &mut (),
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
// Clamp to u32::MAX
|
||||||
|
let length = value.chars().count().min(u32::MAX as usize) as u32;
|
||||||
|
|
||||||
|
if length < self.min_length {
|
||||||
|
errors.push(FieldError::TooShort {
|
||||||
|
current: length,
|
||||||
|
min_length: self.min_length,
|
||||||
|
max_length: self.max_length,
|
||||||
|
unit: FieldUnit::Characters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if length > self.max_length {
|
||||||
|
errors.push(FieldError::TooLong {
|
||||||
|
current: length,
|
||||||
|
min_length: self.min_length,
|
||||||
|
max_length: self.max_length,
|
||||||
|
unit: FieldUnit::Characters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Regex for an e-mail address according to
|
||||||
|
/// <https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address>
|
||||||
|
///
|
||||||
|
/// This is likely representative of what browsers accept, but note that
|
||||||
|
/// it intentionally deviates from RFC 5322 which is not considered practical.
|
||||||
|
static EMAIL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
|
Regex::new(r"^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$").expect("email regex")
|
||||||
|
});
|
||||||
|
|
||||||
|
pub struct Email;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl FieldValidator<()> for Email {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
async fn validate(
|
||||||
|
&self,
|
||||||
|
value: &str,
|
||||||
|
errors: &mut FieldErrors,
|
||||||
|
_context: &mut (),
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
if !EMAIL_REGEX.is_match_at(value, 0) {
|
||||||
|
errors.push(FieldError::Custom {
|
||||||
|
code: "invalid_email".to_owned(),
|
||||||
|
description: "Not a valid e-mail address".to_owned(),
|
||||||
|
values: BTreeMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl FieldValidator<()> for Regex {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
async fn validate(
|
||||||
|
&self,
|
||||||
|
value: &str,
|
||||||
|
errors: &mut FieldErrors,
|
||||||
|
_context: &mut (),
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
if !self.is_match_at(value, 0) {
|
||||||
|
errors.push(FieldError::Custom {
|
||||||
|
code: "regex_unmatched".to_owned(),
|
||||||
|
description: "Field did not match the intended pattern".to_owned(),
|
||||||
|
values: BTreeMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
14
formbeam_derive/Cargo.toml
Normal file
14
formbeam_derive/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "formbeam_derive"
|
||||||
|
description = "Form system for the Hornbeam template engine (derive macros)"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
version = "0.0.5"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = "1.0.86"
|
||||||
|
quote = "1.0.36"
|
||||||
|
syn = "2.0.72"
|
540
formbeam_derive/src/derive_form.rs
Normal file
540
formbeam_derive/src/derive_form.rs
Normal file
@ -0,0 +1,540 @@
|
|||||||
|
use proc_macro2::Ident;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::format_ident;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::parenthesized;
|
||||||
|
use syn::DeriveInput;
|
||||||
|
use syn::Field;
|
||||||
|
use syn::LitInt;
|
||||||
|
use syn::LitStr;
|
||||||
|
use syn::Type;
|
||||||
|
use syn::Visibility;
|
||||||
|
|
||||||
|
pub fn derive_form(input: DeriveInput) -> TokenStream {
|
||||||
|
let fields = match input.data {
|
||||||
|
syn::Data::Struct(strukt) => match strukt.fields {
|
||||||
|
syn::Fields::Named(named) => named,
|
||||||
|
syn::Fields::Unnamed(_) => panic!("cannot derive Form for a tuple struct"),
|
||||||
|
syn::Fields::Unit => panic!("cannot derive Form for a unit struct"),
|
||||||
|
},
|
||||||
|
syn::Data::Enum(_) => panic!("cannot derive Form for an enum"),
|
||||||
|
syn::Data::Union(_) => panic!("cannot derive Form for a union"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut fields_vec = Vec::new();
|
||||||
|
for pair in fields.named.into_pairs() {
|
||||||
|
let field = pair.into_value();
|
||||||
|
let f_info = parse_field_attrs(&field);
|
||||||
|
fields_vec.push((field, f_info));
|
||||||
|
}
|
||||||
|
|
||||||
|
let partial_ident = format_ident!("{}Raw", input.ident);
|
||||||
|
let validation_ident = format_ident!("{}Validation", input.ident);
|
||||||
|
|
||||||
|
let partial_struct = write_form_partial_struct(&partial_ident, &fields_vec, &input.vis);
|
||||||
|
let validation_struct =
|
||||||
|
write_form_validation_struct(&partial_ident, &validation_ident, &fields_vec, &input.vis);
|
||||||
|
let partial_impl =
|
||||||
|
write_form_partial_impl(&input.ident, &partial_ident, &validation_ident, &fields_vec);
|
||||||
|
|
||||||
|
quote!(
|
||||||
|
#partial_struct
|
||||||
|
#validation_struct
|
||||||
|
|
||||||
|
#partial_impl
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn identify_base_type(unprefixed: &Ident) -> FieldType {
|
||||||
|
match unprefixed.to_string().as_str() {
|
||||||
|
"u8" | "i8" | "u16" | "i16" | "u32" | "i32" | "u64" | "i64" | "u128" | "i128" | "f32"
|
||||||
|
| "f64" => FieldType::Numeric,
|
||||||
|
"String" => FieldType::String,
|
||||||
|
"bool" => FieldType::Boolean,
|
||||||
|
other => {
|
||||||
|
panic!("unsupported field type: `{other}`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn identify_type(ty: &Type) -> (FieldType, bool) {
|
||||||
|
let (ftype, needed) = match ty {
|
||||||
|
syn::Type::Array(_) => panic!("unsupported field type: array"),
|
||||||
|
syn::Type::BareFn(_) => panic!("unsupported field type: bare fn"),
|
||||||
|
syn::Type::Group(_) => panic!("unsupported field type: Group"),
|
||||||
|
syn::Type::ImplTrait(_) => panic!("unsupported field type: impl Trait"),
|
||||||
|
syn::Type::Infer(_) => panic!("unsupported field type: Infer"),
|
||||||
|
syn::Type::Macro(_) => panic!("unsupported field type: macro"),
|
||||||
|
syn::Type::Never(_) => panic!("unsupported field type: Never"),
|
||||||
|
syn::Type::Paren(_) => panic!("unsupported field type: parenthesised"),
|
||||||
|
syn::Type::Path(type_path) => {
|
||||||
|
let path = &type_path.path;
|
||||||
|
if path.segments.len() != 1 {
|
||||||
|
panic!("unsupported field type syntax (can't deal with multiple path segments)");
|
||||||
|
}
|
||||||
|
let segment = path.segments.get(0).unwrap();
|
||||||
|
|
||||||
|
if segment.ident == "Option" {
|
||||||
|
match &segment.arguments {
|
||||||
|
syn::PathArguments::None => {
|
||||||
|
panic!("unsupported field type: Option without args")
|
||||||
|
}
|
||||||
|
syn::PathArguments::AngleBracketed(bracks) => {
|
||||||
|
if bracks.args.len() != 1 {
|
||||||
|
panic!("unsupported field type: Option with other than 1 arg");
|
||||||
|
}
|
||||||
|
let arg = bracks.args.get(0).unwrap();
|
||||||
|
match arg {
|
||||||
|
syn::GenericArgument::Lifetime(_) => {
|
||||||
|
panic!("unsupported field type with lifetime")
|
||||||
|
}
|
||||||
|
syn::GenericArgument::Type(ty) => {
|
||||||
|
let (base_type, check_needed) = identify_type(ty);
|
||||||
|
if base_type == FieldType::Boolean {
|
||||||
|
panic!(
|
||||||
|
"Option<bool> isn't supported (does it even make sense?)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !check_needed {
|
||||||
|
panic!("unsupported field type with nested Options or something like that");
|
||||||
|
}
|
||||||
|
(base_type, false)
|
||||||
|
}
|
||||||
|
syn::GenericArgument::Const(_) => {
|
||||||
|
panic!("unsupported field type with const")
|
||||||
|
}
|
||||||
|
syn::GenericArgument::AssocType(_) => {
|
||||||
|
panic!("unsupported field type with associated type")
|
||||||
|
}
|
||||||
|
syn::GenericArgument::AssocConst(_) => {
|
||||||
|
panic!("unsupported field type with associated const")
|
||||||
|
}
|
||||||
|
syn::GenericArgument::Constraint(_) => {
|
||||||
|
panic!("unsupported field type with constraint")
|
||||||
|
}
|
||||||
|
_ => panic!("other generic argument unsupported?"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syn::PathArguments::Parenthesized(_) => {
|
||||||
|
panic!("unsupported field type with ( )s")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match &segment.arguments {
|
||||||
|
syn::PathArguments::None => {
|
||||||
|
let base_type = identify_base_type(&segment.ident);
|
||||||
|
let needed = base_type != FieldType::Boolean;
|
||||||
|
(base_type, needed)
|
||||||
|
}
|
||||||
|
syn::PathArguments::AngleBracketed(_) => {
|
||||||
|
panic!("unsupported field type: non-Option with args")
|
||||||
|
}
|
||||||
|
syn::PathArguments::Parenthesized(_) => {
|
||||||
|
panic!("unsupported field type with ( )s")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syn::Type::Ptr(_) => panic!("unsupported field type: pointer"),
|
||||||
|
syn::Type::Reference(_) => panic!("unsupported field type: ref"),
|
||||||
|
syn::Type::Slice(_) => panic!("unsupported field type: slice"),
|
||||||
|
syn::Type::TraitObject(_) => panic!("unsupported field type: trait object"),
|
||||||
|
syn::Type::Tuple(_) => panic!("unsupported field type: tuple"),
|
||||||
|
syn::Type::Verbatim(_) => panic!("verbatim?"),
|
||||||
|
_ => panic!("unsupported (unknown) field type"),
|
||||||
|
};
|
||||||
|
(ftype, needed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_field_attrs(f: &Field) -> FieldInfo {
|
||||||
|
let (ftype, needed) = identify_type(&f.ty);
|
||||||
|
let mut out = FieldInfo {
|
||||||
|
ftype,
|
||||||
|
needed,
|
||||||
|
need_nonempty: true,
|
||||||
|
min_chars: None,
|
||||||
|
max_chars: None,
|
||||||
|
email: false,
|
||||||
|
regex: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
for attr in &f.attrs {
|
||||||
|
if !attr.meta.path().is_ident("form") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
attr.parse_nested_meta(|meta| {
|
||||||
|
// #[form(allow_empty)]
|
||||||
|
if meta.path.is_ident("allow_empty") {
|
||||||
|
out.need_nonempty = false;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[form(email)]
|
||||||
|
if meta.path.is_ident("email") {
|
||||||
|
if out.email {
|
||||||
|
panic!("duplicate email annotation");
|
||||||
|
}
|
||||||
|
out.email = true;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[form(min_chars(2))]
|
||||||
|
if meta.path.is_ident("min_chars") {
|
||||||
|
let content;
|
||||||
|
parenthesized!(content in meta.input);
|
||||||
|
let lit: LitInt = content.parse()?;
|
||||||
|
let n: u32 = lit.base10_parse()?;
|
||||||
|
if out.min_chars.is_some() {
|
||||||
|
panic!("duplicate min_chars annotation");
|
||||||
|
}
|
||||||
|
out.min_chars = Some(n);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[form(max_chars(2))]
|
||||||
|
if meta.path.is_ident("max_chars") {
|
||||||
|
let content;
|
||||||
|
parenthesized!(content in meta.input);
|
||||||
|
let lit: LitInt = content.parse()?;
|
||||||
|
let n: u32 = lit.base10_parse()?;
|
||||||
|
if out.max_chars.is_some() {
|
||||||
|
panic!("duplicate max_chars annotation");
|
||||||
|
}
|
||||||
|
out.max_chars = Some(n);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[form(regex("\A[0-9]+\Z"))]
|
||||||
|
if meta.path.is_ident("regex") {
|
||||||
|
let content;
|
||||||
|
parenthesized!(content in meta.input);
|
||||||
|
let lit: LitStr = content.parse()?;
|
||||||
|
if out.regex.is_some() {
|
||||||
|
panic!("duplicate regex annotation");
|
||||||
|
}
|
||||||
|
out.regex = Some(lit.value());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!(
|
||||||
|
"unrecognised field attribute: {}",
|
||||||
|
meta.path
|
||||||
|
.get_ident()
|
||||||
|
.map(|i| format!("`{i}`"))
|
||||||
|
.unwrap_or_else(|| "<???>".to_owned())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_form_partial_struct(
|
||||||
|
partial_ident: &Ident,
|
||||||
|
fields: &[(Field, FieldInfo)],
|
||||||
|
vis: &Visibility,
|
||||||
|
) -> TokenStream {
|
||||||
|
let field_idents: Vec<&Ident> = fields
|
||||||
|
.iter()
|
||||||
|
.map(|(f, _)| f.ident.as_ref().unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
quote!(
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Default, Debug, ::bevy_reflect::Reflect, ::serde::Deserialize)]
|
||||||
|
#vis struct #partial_ident {
|
||||||
|
#(#field_idents: Option<String>),*
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_form_validation_struct(
|
||||||
|
partial_ident: &Ident,
|
||||||
|
validation_ident: &Ident,
|
||||||
|
fields: &[(Field, FieldInfo)],
|
||||||
|
vis: &Visibility,
|
||||||
|
) -> TokenStream {
|
||||||
|
let field_idents: Vec<&Ident> = fields
|
||||||
|
.iter()
|
||||||
|
.map(|(f, _)| f.ident.as_ref().unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
quote!(
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Default, Debug, ::bevy_reflect::Reflect)]
|
||||||
|
#vis struct #validation_ident {
|
||||||
|
form_wide: ::formbeam::FieldErrors,
|
||||||
|
#(#field_idents: ::formbeam::FieldErrors),*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::formbeam::traits::FormValidation for #validation_ident {
|
||||||
|
type Partial = #partial_ident;
|
||||||
|
|
||||||
|
fn is_valid(&self) -> bool {
|
||||||
|
self.form_wide.is_empty()
|
||||||
|
#(&& self.#field_idents.is_empty())*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_form_partial_impl(
|
||||||
|
form_ident: &Ident,
|
||||||
|
partial_ident: &Ident,
|
||||||
|
validation_ident: &Ident,
|
||||||
|
fields: &[(Field, FieldInfo)],
|
||||||
|
) -> TokenStream {
|
||||||
|
let validate_body = write_partial_impl_validate(validation_ident, fields);
|
||||||
|
let form_method_body = write_partial_impl_form_method(form_ident, fields);
|
||||||
|
let form_info = write_form_info(fields);
|
||||||
|
quote!(
|
||||||
|
impl ::formbeam::Form for #form_ident {
|
||||||
|
type Partial = #partial_ident;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[::async_trait::async_trait]
|
||||||
|
impl ::formbeam::FormPartial for #partial_ident {
|
||||||
|
type Form = #form_ident;
|
||||||
|
type Validation = #validation_ident;
|
||||||
|
type Error = ::core::convert::Infallible; // TODO
|
||||||
|
|
||||||
|
const INFO: &'static ::formbeam::FormPartialInfo = &#form_info;
|
||||||
|
|
||||||
|
async fn validate(&self) -> Result<Self::Validation, Self::Error> {
|
||||||
|
use ::formbeam::FieldValidator;
|
||||||
|
#validate_body
|
||||||
|
}
|
||||||
|
|
||||||
|
fn form(&self) -> Result<Self::Form, &'static str> {
|
||||||
|
#form_method_body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_partial_impl_validate(
|
||||||
|
validation_ident: &Ident,
|
||||||
|
fields: &[(Field, FieldInfo)],
|
||||||
|
) -> TokenStream {
|
||||||
|
let field_statements = fields.iter().map(write_validate_statement);
|
||||||
|
quote!(
|
||||||
|
let mut errors = #validation_ident::default();
|
||||||
|
|
||||||
|
#(#field_statements)*
|
||||||
|
|
||||||
|
Ok(errors)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FieldInfo {
|
||||||
|
ftype: FieldType,
|
||||||
|
/// Whether the field is required to be present in the form.
|
||||||
|
/// This is true unless the type is `Option<T>`.
|
||||||
|
needed: bool,
|
||||||
|
/// Whether the field needs to be non-empty. By default this is true.
|
||||||
|
/// If `needed` is false and `need_nonempty` is true, then empty fields will be coerced to `None` instead.
|
||||||
|
/// If both `needed` and `need_nonempty` are false, then empty fields will be `Some("")` o.e.; only absent fields will be `None`.
|
||||||
|
/// If both `needed` and `need_nonempty` are true, then empty fields will not be accepted
|
||||||
|
/// and the `Required` validator is added to the field's info (this matches HTML form semantics).
|
||||||
|
need_nonempty: bool,
|
||||||
|
min_chars: Option<u32>,
|
||||||
|
max_chars: Option<u32>,
|
||||||
|
email: bool,
|
||||||
|
regex: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||||
|
enum FieldType {
|
||||||
|
Numeric,
|
||||||
|
String,
|
||||||
|
/// Boolean indicating the presence and non-emptiness of the field.
|
||||||
|
Boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_form_info(fields: &[(Field, FieldInfo)]) -> TokenStream {
|
||||||
|
let form_validators: Vec<TokenStream> = Vec::new();
|
||||||
|
let fields: Vec<TokenStream> = fields
|
||||||
|
.iter()
|
||||||
|
.map(|(f, fi)| write_form_info_for_field(f, fi))
|
||||||
|
.collect();
|
||||||
|
quote!(
|
||||||
|
::formbeam::FormPartialInfo {
|
||||||
|
form_validators: &[
|
||||||
|
#(#form_validators),*
|
||||||
|
],
|
||||||
|
fields: &[
|
||||||
|
#(#fields),*
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_form_info_for_field(field: &Field, f_info: &FieldInfo) -> TokenStream {
|
||||||
|
let name = field.ident.as_ref().unwrap().to_string();
|
||||||
|
let mut validators = Vec::new();
|
||||||
|
|
||||||
|
if f_info.needed && f_info.need_nonempty {
|
||||||
|
validators.push(quote!(::formbeam::FieldValidatorInfo::Required));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(l) = f_info.min_chars {
|
||||||
|
validators.push(quote!(::formbeam::FieldValidatorInfo::MinLength(#l)));
|
||||||
|
}
|
||||||
|
if let Some(l) = f_info.max_chars {
|
||||||
|
validators.push(quote!(::formbeam::FieldValidatorInfo::MaxLength(#l)));
|
||||||
|
}
|
||||||
|
if f_info.email {
|
||||||
|
validators.push(quote!(::formbeam::FieldValidatorInfo::Email));
|
||||||
|
}
|
||||||
|
if let Some(r) = &f_info.regex {
|
||||||
|
validators.push(quote!(::formbeam::FieldValidatorInfo::Regex(#r)));
|
||||||
|
}
|
||||||
|
|
||||||
|
quote!(
|
||||||
|
::formbeam::FieldInfo {
|
||||||
|
name: #name,
|
||||||
|
validators: &[#(#validators),*]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_validate_statement((field, f_info): &(Field, FieldInfo)) -> TokenStream {
|
||||||
|
let f = field.ident.as_ref().unwrap();
|
||||||
|
|
||||||
|
let configured_validators = write_validators(field, f_info);
|
||||||
|
|
||||||
|
let check_nonempty = if f_info.need_nonempty {
|
||||||
|
quote!(
|
||||||
|
if field.is_empty() {
|
||||||
|
errors.#f.push(::formbeam::FieldError::Missing);
|
||||||
|
} else {
|
||||||
|
#(#configured_validators)*
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
quote!(#(#configured_validators)*)
|
||||||
|
};
|
||||||
|
|
||||||
|
let else_not_present = if f_info.needed {
|
||||||
|
quote!(
|
||||||
|
errors.#f.push(::formbeam::FieldError::Missing);
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
TokenStream::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
quote!(
|
||||||
|
if let Some(field) = &self.#f {
|
||||||
|
#check_nonempty
|
||||||
|
} else {
|
||||||
|
#else_not_present
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_validators(field: &Field, f_info: &FieldInfo) -> Vec<TokenStream> {
|
||||||
|
let f = field.ident.as_ref().unwrap();
|
||||||
|
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
if f_info.min_chars.is_some() || f_info.max_chars.is_some() {
|
||||||
|
let effective_min = f_info.min_chars.unwrap_or(0);
|
||||||
|
let effective_max = f_info.max_chars.unwrap_or(u32::MAX);
|
||||||
|
|
||||||
|
out.push(quote!(
|
||||||
|
::formbeam::validators::LengthInChars { min_length: #effective_min, max_length: #effective_max }.validate(&field, &mut errors.#f, &mut ()).await?;
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(regex) = &f_info.regex {
|
||||||
|
let regex_ident = format_ident!("REGEX_{}", f.to_string().to_uppercase());
|
||||||
|
out.push(quote!(
|
||||||
|
static #regex_ident: ::std::sync::LazyLock<::formbeam::validators::Regex> = ::std::sync::LazyLock::new(|| {
|
||||||
|
::formbeam::validators::Regex::new(#regex).expect("invalid regex")
|
||||||
|
});
|
||||||
|
#regex_ident.validate(&field, &mut errors.#f, &mut ()).await?;
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if f_info.email {
|
||||||
|
out.push(quote!(
|
||||||
|
::formbeam::validators::Email.validate(&field, &mut errors.#f, &mut ()).await?;
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_partial_impl_form_method(
|
||||||
|
form_ident: &Ident,
|
||||||
|
fields: &[(Field, FieldInfo)],
|
||||||
|
) -> TokenStream {
|
||||||
|
let field_idents: Vec<&Ident> = fields
|
||||||
|
.iter()
|
||||||
|
.map(|(f, _)| f.ident.as_ref().unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut statements = Vec::new();
|
||||||
|
|
||||||
|
for (field, field_info) in fields.iter() {
|
||||||
|
let f = field.ident.as_ref().unwrap();
|
||||||
|
|
||||||
|
let none_case = if field_info.needed {
|
||||||
|
let f_name = f.to_string();
|
||||||
|
quote!(return Err(#f_name);)
|
||||||
|
} else if field_info.ftype == FieldType::Boolean {
|
||||||
|
quote!(false)
|
||||||
|
} else {
|
||||||
|
quote!(None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let some_case = {
|
||||||
|
let converter = converter_from_raw_to_field(f, field_info);
|
||||||
|
|
||||||
|
if field_info.needed || field_info.ftype == FieldType::Boolean {
|
||||||
|
converter
|
||||||
|
} else if field_info.need_nonempty {
|
||||||
|
// For not-needed fields that must not be empty,
|
||||||
|
// coerce empty fields into `None`
|
||||||
|
quote!(
|
||||||
|
if raw.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some({#converter})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
quote!(Some({#converter}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
statements.push(quote!(
|
||||||
|
let #f = match &self.#f {
|
||||||
|
Some(raw) => {
|
||||||
|
#some_case
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
#none_case
|
||||||
|
}
|
||||||
|
};
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
quote!(
|
||||||
|
#(#statements)*
|
||||||
|
Ok(#form_ident { #(#field_idents),* })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn converter_from_raw_to_field(f: &Ident, field_info: &FieldInfo) -> TokenStream {
|
||||||
|
let f_name = f.to_string();
|
||||||
|
match field_info.ftype {
|
||||||
|
FieldType::Numeric => quote!(
|
||||||
|
raw.parse().map_err(|_| #f_name)?
|
||||||
|
),
|
||||||
|
FieldType::String => quote!(raw.to_owned()),
|
||||||
|
FieldType::Boolean => quote!(!raw.is_empty()),
|
||||||
|
}
|
||||||
|
}
|
10
formbeam_derive/src/lib.rs
Normal file
10
formbeam_derive/src/lib.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use syn::parse_macro_input;
|
||||||
|
|
||||||
|
mod derive_form;
|
||||||
|
|
||||||
|
#[proc_macro_derive(Form, attributes(form))]
|
||||||
|
pub fn my_proc_macro(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as syn::DeriveInput);
|
||||||
|
derive_form::derive_form(input).into()
|
||||||
|
}
|
@ -1,21 +1,24 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "hornbeam"
|
name = "hornbeam"
|
||||||
version = "0.0.1"
|
description = "Hornbeam template engine (high-level crate for use in applications)"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
version = "0.0.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hornbeam_interpreter = { version = "0.0.1", path = "../hornbeam_interpreter" }
|
hornbeam_interpreter = { version = "0.0.5", path = "../hornbeam_interpreter" }
|
||||||
|
|
||||||
arc-swap = "1.6.0"
|
arc-swap = "1.6.0"
|
||||||
notify = "5.1.0"
|
notify = "5.1.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
|
||||||
tokio = { version = "1.26.0", optional = true }
|
tokio = { version = "1.26.0", optional = true }
|
||||||
axum = { version = "0.6.10", optional = true }
|
axum = { version = "0.8.4", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["interpreted", "hot_reload"]
|
default = ["interpreted", "hot_reload"]
|
||||||
interpreted = []
|
interpreted = []
|
||||||
hot_reload = ["tokio", "axum"]
|
hot_reload = ["tokio", "axum"]
|
||||||
|
formbeam = ["hornbeam_interpreter/formbeam"]
|
||||||
|
@ -70,14 +70,18 @@ pub fn new_template_manager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
successful.expect(&format!("Could not find Hornbeam templates/translations: tried looking in {try_paths:?} and no HORNBEAM_BASE environment variable set!"))
|
successful.unwrap_or_else(|| panic!("Could not find Hornbeam templates/translations: tried looking in {try_paths:?} and no HORNBEAM_BASE environment variable set!"))
|
||||||
};
|
};
|
||||||
let template_sys = load_new_template_system(
|
let template_sys = match load_new_template_system(
|
||||||
default_locale,
|
default_locale,
|
||||||
&base_dir.join("templates"),
|
&base_dir.join("templates"),
|
||||||
&base_dir.join("translations"),
|
&base_dir.join("translations"),
|
||||||
)
|
) {
|
||||||
.expect("Failed to create Hornbeam environment!");
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
panic!("Failed to create Hornbeam environment! {err}");
|
||||||
|
}
|
||||||
|
};
|
||||||
let templates = Arc::new(ArcSwap::new(Arc::new(template_sys)));
|
let templates = Arc::new(ArcSwap::new(Arc::new(template_sys)));
|
||||||
|
|
||||||
#[cfg(feature = "hot_reload")]
|
#[cfg(feature = "hot_reload")]
|
||||||
@ -111,6 +115,11 @@ fn load_new_template_system(
|
|||||||
|
|
||||||
#[cfg(feature = "hot_reload")]
|
#[cfg(feature = "hot_reload")]
|
||||||
pub fn is_hot_reload_enabled() -> bool {
|
pub fn is_hot_reload_enabled() -> bool {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
const DEFAULT_HOT_RELOAD: bool = true;
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
const DEFAULT_HOT_RELOAD: bool = false;
|
||||||
|
|
||||||
std::env::var("HORNBEAM_HOT")
|
std::env::var("HORNBEAM_HOT")
|
||||||
.map(|env_var| {
|
.map(|env_var| {
|
||||||
if let Ok(i) = env_var.parse::<u32>() {
|
if let Ok(i) = env_var.parse::<u32>() {
|
||||||
@ -126,9 +135,9 @@ pub fn is_hot_reload_enabled() -> bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
eprintln!("Not sure how to interpret HORNBEAM_HOT={env_var:?}, assuming yes.");
|
eprintln!("Not sure how to interpret HORNBEAM_HOT={env_var:?}, assuming yes.");
|
||||||
return true;
|
true
|
||||||
})
|
})
|
||||||
.unwrap_or(true)
|
.unwrap_or(DEFAULT_HOT_RELOAD)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "hot_reload"))]
|
#[cfg(not(feature = "hot_reload"))]
|
||||||
|
@ -6,11 +6,13 @@ use axum::{Extension, Router};
|
|||||||
use hornbeam_interpreter::localisation::fluent::FluentLocalisationSystem;
|
use hornbeam_interpreter::localisation::fluent::FluentLocalisationSystem;
|
||||||
use hornbeam_interpreter::LoadedTemplates;
|
use hornbeam_interpreter::LoadedTemplates;
|
||||||
use notify::{Event, EventKind, RecursiveMode, Watcher};
|
use notify::{Event, EventKind, RecursiveMode, Watcher};
|
||||||
|
use std::error::Error;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::mpsc::RecvTimeoutError;
|
use std::sync::mpsc::RecvTimeoutError;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
pub(crate) fn start_hot_reloader(
|
pub(crate) fn start_hot_reloader(
|
||||||
@ -56,7 +58,7 @@ pub(crate) fn start_hot_reloader(
|
|||||||
let default_locale = default_locale;
|
let default_locale = default_locale;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let _ = notif_rx.recv().unwrap();
|
notif_rx.recv().unwrap();
|
||||||
|
|
||||||
// Debounce, because editors often make a series of modifications and we don't
|
// Debounce, because editors often make a series of modifications and we don't
|
||||||
// want to reload before it's ready.
|
// want to reload before it's ready.
|
||||||
@ -100,14 +102,17 @@ pub(crate) fn start_auto_hot_reloader() -> WaiterList {
|
|||||||
.layer(Extension(waiter_list.clone()));
|
.layer(Extension(waiter_list.clone()));
|
||||||
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], 7015));
|
let addr = SocketAddr::from(([127, 0, 0, 1], 7015));
|
||||||
eprintln!("Hornbeam Auto Hot Reload: Listening on http://{}", addr);
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = axum::Server::bind(&addr)
|
let result: Result<(), Box<dyn Error>> = async move {
|
||||||
.serve(app.into_make_service())
|
let listener = TcpListener::bind(addr).await?;
|
||||||
.await
|
eprintln!("Hornbeam Auto Hot Reload: Listening on http://{}", addr);
|
||||||
{
|
axum::serve(listener, app).await?;
|
||||||
eprintln!("Hornbeam Auto Hot Reload failed: {e:?}");
|
Ok(())
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
if let Err(e) = result {
|
||||||
|
eprintln!("Hornbeam Auto Hot Reload failed: {e}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -17,3 +17,7 @@ pub use interpreted::{
|
|||||||
is_hot_reload_enabled, lazy_static, new_template_manager, Params, TemplateError,
|
is_hot_reload_enabled, lazy_static, new_template_manager, Params, TemplateError,
|
||||||
TemplateManager,
|
TemplateManager,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "formbeam")]
|
||||||
|
#[cfg(feature = "interpreted")]
|
||||||
|
pub use hornbeam_interpreter::formbeam_integration::ReflectedForm;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "hornbeam_grammar"
|
name = "hornbeam_grammar"
|
||||||
version = "0.0.1"
|
description = "Grammar for the Hornbeam template language"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
version = "0.0.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
@ -4,17 +4,27 @@ use std::collections::BTreeMap;
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||||
pub struct Template {
|
pub struct Template {
|
||||||
|
pub param_defs: Option<Vec<ParameterDefinition>>,
|
||||||
pub blocks: Vec<Block>,
|
pub blocks: Vec<Block>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct ParameterDefinition {
|
||||||
|
pub name: IStr,
|
||||||
|
pub loc: Locator,
|
||||||
|
pub default: Option<Expression>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||||
pub enum Block {
|
pub enum Block {
|
||||||
HtmlElement(HtmlElement),
|
HtmlElement(HtmlElement),
|
||||||
ComponentElement(ComponentElement),
|
ComponentElement(ComponentElement),
|
||||||
|
SetStatement(SetStatement),
|
||||||
IfBlock(IfBlock),
|
IfBlock(IfBlock),
|
||||||
ForBlock(ForBlock),
|
ForBlock(ForBlock),
|
||||||
MatchBlock(MatchBlock),
|
MatchBlock(MatchBlock),
|
||||||
Text(StringExpr),
|
Text(StringExpr),
|
||||||
|
RawUnescapedHtml(StringExpr),
|
||||||
DefineExpandSlot(DefineExpandSlot),
|
DefineExpandSlot(DefineExpandSlot),
|
||||||
DefineFragment(DefineFragment),
|
DefineFragment(DefineFragment),
|
||||||
}
|
}
|
||||||
@ -25,10 +35,15 @@ pub struct HtmlElement {
|
|||||||
pub children: Vec<Block>,
|
pub children: Vec<Block>,
|
||||||
pub classes: Vec<IStr>,
|
pub classes: Vec<IStr>,
|
||||||
pub dom_id: Option<IStr>,
|
pub dom_id: Option<IStr>,
|
||||||
pub attributes: BTreeMap<IStr, Expression>,
|
pub attributes: BTreeMap<IStr, (Expression, ElementAttributeFlags)>,
|
||||||
pub loc: Locator,
|
pub loc: Locator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct ElementAttributeFlags {
|
||||||
|
pub optional: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||||
pub struct ComponentElement {
|
pub struct ComponentElement {
|
||||||
pub name: IStr,
|
pub name: IStr,
|
||||||
@ -37,6 +52,13 @@ pub struct ComponentElement {
|
|||||||
pub loc: Locator,
|
pub loc: Locator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct SetStatement {
|
||||||
|
pub binding: Binding,
|
||||||
|
pub expression: Expression,
|
||||||
|
pub loc: Locator,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||||
pub struct IfBlock {
|
pub struct IfBlock {
|
||||||
pub condition: Expression,
|
pub condition: Expression,
|
||||||
@ -69,6 +91,11 @@ pub enum MatchBinding {
|
|||||||
/// `None =>`
|
/// `None =>`
|
||||||
UnitVariant { name: IStr },
|
UnitVariant { name: IStr },
|
||||||
|
|
||||||
|
/// `$var`
|
||||||
|
/// (A fallback case)
|
||||||
|
/// TODO this is not implemented in the grammar yet, but is used in generated rules
|
||||||
|
Variable { name: IStr },
|
||||||
|
|
||||||
/// `_ =>`
|
/// `_ =>`
|
||||||
Ignore,
|
Ignore,
|
||||||
}
|
}
|
||||||
@ -76,6 +103,7 @@ pub enum MatchBinding {
|
|||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||||
pub enum Binding {
|
pub enum Binding {
|
||||||
Variable(IStr),
|
Variable(IStr),
|
||||||
|
Tuple(Vec<Binding>),
|
||||||
Ignore,
|
Ignore,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +177,14 @@ pub enum Expression {
|
|||||||
left: Box<Expression>,
|
left: Box<Expression>,
|
||||||
right: Box<Expression>,
|
right: Box<Expression>,
|
||||||
},
|
},
|
||||||
|
LessThan {
|
||||||
|
left: Box<Expression>,
|
||||||
|
right: Box<Expression>,
|
||||||
|
},
|
||||||
|
LessThanOrEquals {
|
||||||
|
left: Box<Expression>,
|
||||||
|
right: Box<Expression>,
|
||||||
|
},
|
||||||
|
|
||||||
// Other Operators
|
// Other Operators
|
||||||
ListAdd {
|
ListAdd {
|
||||||
@ -163,6 +199,8 @@ pub enum Expression {
|
|||||||
IntLiteral {
|
IntLiteral {
|
||||||
val: i64,
|
val: i64,
|
||||||
},
|
},
|
||||||
|
BoolLiteral(bool),
|
||||||
|
NoneLiteral,
|
||||||
StringExpr(StringExpr),
|
StringExpr(StringExpr),
|
||||||
|
|
||||||
// Relatives
|
// Relatives
|
||||||
@ -188,4 +226,9 @@ pub enum Expression {
|
|||||||
args: Vec<Expression>,
|
args: Vec<Expression>,
|
||||||
loc: Locator,
|
loc: Locator,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Unwrap {
|
||||||
|
obj: Box<Expression>,
|
||||||
|
loc: Locator,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,21 @@
|
|||||||
//
|
//
|
||||||
Hornbeam = { SOI ~ wsnl* ~ BlockContent* ~ ws* ~ EOI }
|
Hornbeam = { SOI ~ wsnl* ~ PreambleList? ~ wsnl* ~ HornbeamBlockList ~ ws* ~ EOI }
|
||||||
|
|
||||||
|
// Will eventually expand to other types of preamble content
|
||||||
|
PreambleList = {
|
||||||
|
// accept `declare` keyword. We expect `PEEK_ALL` to be empty, but future definitions might change this.
|
||||||
|
PEEK_ALL ~ "declare" ~ lineEnd ~
|
||||||
|
// then accept the first definition. We must have at least one definition.
|
||||||
|
// We accept the new indentation level with the `PUSH` here
|
||||||
|
PEEK_ALL ~ PUSH(" "+ | "\t"+) ~ ParameterDefinition ~
|
||||||
|
// Now accept any number of extra definitions at the same indentation level.
|
||||||
|
(PEEK_ALL ~ ParameterDefinition)* ~
|
||||||
|
// Drop the indentation when exiting the preamble block
|
||||||
|
DROP
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
HornbeamBlockList = { BlockContent* }
|
||||||
|
|
||||||
NewBlock = _{
|
NewBlock = _{
|
||||||
PEEK_ALL ~ PUSH(" "+ | "\t"+) ~ BlockContent ~
|
PEEK_ALL ~ PUSH(" "+ | "\t"+) ~ BlockContent ~
|
||||||
@ -10,14 +26,23 @@ BlockContent = _{
|
|||||||
Element |
|
Element |
|
||||||
IfBlock |
|
IfBlock |
|
||||||
Text |
|
Text |
|
||||||
|
RawUnescapedHtml |
|
||||||
DefineExpandSlot |
|
DefineExpandSlot |
|
||||||
|
SetStatement |
|
||||||
ForBlock |
|
ForBlock |
|
||||||
MatchBlock |
|
MatchBlock |
|
||||||
DefineFragment
|
DefineFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `param $x`
|
||||||
|
// `param $x = 1 + 1`
|
||||||
|
// TODO: type annotation: `param $x: int`
|
||||||
|
ParameterDefinition = {
|
||||||
|
"param" ~ ws+ ~ "$" ~ Identifier ~ (ws* ~ "=" ~ ws* ~ Expr)? ~ lineEnd
|
||||||
|
}
|
||||||
|
|
||||||
Element = {
|
Element = {
|
||||||
ElementName ~ cssClass* ~ domId? ~ (ws+ ~ MapLiteral)? ~ lineEnd ~ (NewBlock | NewSlotBlock)?
|
ElementName ~ cssClass* ~ domId? ~ (ws+ ~ AttrMapLiteral)? ~ lineEnd ~ (NewBlock | NewSlotBlock)?
|
||||||
}
|
}
|
||||||
|
|
||||||
cssClass = _{
|
cssClass = _{
|
||||||
@ -34,6 +59,14 @@ Text = {
|
|||||||
String ~ lineEnd
|
String ~ lineEnd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RawUnescapedHtml = {
|
||||||
|
"raw" ~ ws+ ~ String ~ lineEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
SetStatement = {
|
||||||
|
"set" ~ ws+ ~ Binding ~ ws+ ~ "=" ~ ws+ ~ Expr ~ lineEnd
|
||||||
|
}
|
||||||
|
|
||||||
IfBlock = {
|
IfBlock = {
|
||||||
"if" ~ ws+ ~ IfCondition ~ lineEnd ~ NewBlock ~
|
"if" ~ ws+ ~ IfCondition ~ lineEnd ~ NewBlock ~
|
||||||
ElseBlock?
|
ElseBlock?
|
||||||
@ -125,20 +158,21 @@ lineEnd = _{ ws_nocnl* ~ nlOrEoi ~ (ws_nocnl* ~ NEWLINE)* ~ (ws_nocnl* ~ &EOI)?
|
|||||||
// was: lineEnd = _{ ws_nocnl* ~ nlOrEoi ~ (!EOI ~ ws* ~ nlOrEoi)* }
|
// was: lineEnd = _{ ws_nocnl* ~ nlOrEoi ~ (!EOI ~ ws* ~ nlOrEoi)* }
|
||||||
|
|
||||||
SingleStringContent = { (!("'" | "\\" | "$") ~ ANY)+ }
|
SingleStringContent = { (!("'" | "\\" | "$") ~ ANY)+ }
|
||||||
singleString = _{ !("''") ~ "'" ~ (SingleStringContent | SEscape | SInterpol)* ~ "'" }
|
singleString = _{ !("''") ~ "'" ~ (SingleStringContent | SEscape | SSimpleVarInterpol | SInterpol)* ~ "'" }
|
||||||
|
|
||||||
DoubleStringContent = { (!("\"" | "\\" | "$") ~ ANY)+ }
|
DoubleStringContent = { (!("\"" | "\\" | "$") ~ ANY)+ }
|
||||||
doubleString = _{ "\"" ~ (DoubleStringContent | SEscape | SInterpol)* ~ "\"" }
|
doubleString = _{ "\"" ~ (DoubleStringContent | SEscape | SSimpleVarInterpol | SInterpol)* ~ "\"" }
|
||||||
|
|
||||||
blockStringStart = _{ "''" ~ NEWLINE ~ PEEK_ALL }
|
blockStringStart = _{ "''" ~ NEWLINE ~ PEEK_ALL }
|
||||||
blockStringEnd = _{ NEWLINE ~ (" " | "\t")* ~ "''" }
|
blockStringEnd = _{ NEWLINE ~ (" " | "\t")* ~ "''" }
|
||||||
BlockStringContent = { (!(NEWLINE | blockStringEnd | "\\" | "$" | "@") ~ ANY)+ }
|
BlockStringContent = { (!(NEWLINE | blockStringEnd | "\\" | "$" | "@") ~ ANY)+ }
|
||||||
// This rule becomes just \n later on, so it effectively strips leading indentation!
|
// This rule becomes just \n later on, so it effectively strips leading indentation!
|
||||||
BlockStringNewline = { !blockStringEnd ~ NEWLINE ~ PEEK_ALL }
|
BlockStringNewline = { !blockStringEnd ~ NEWLINE ~ PEEK_ALL }
|
||||||
blockString = _{ blockStringStart ~ (BlockStringContent | SEscape | SInterpol | ParameterisedLocalisation | BlockStringNewline)* ~ blockStringEnd }
|
blockString = _{ blockStringStart ~ (BlockStringContent | SEscape | SSimpleVarInterpol | SInterpol | ParameterisedLocalisation | BlockStringNewline)* ~ blockStringEnd }
|
||||||
String = { blockString | singleString | doubleString | ParameterisedLocalisation }
|
String = { blockString | singleString | doubleString | ParameterisedLocalisation }
|
||||||
|
|
||||||
SEscape = { "\\" ~ ("\\" | "'" | "\"" | "$" | "@") }
|
SEscape = { "\\" ~ ("\\" | "'" | "\"" | "$" | "@") }
|
||||||
|
SSimpleVarInterpol = { "$" ~ Identifier }
|
||||||
SInterpol = { "${" ~ ws* ~ Expr ~ ws* ~ "}" }
|
SInterpol = { "${" ~ ws* ~ Expr ~ ws* ~ "}" }
|
||||||
|
|
||||||
|
|
||||||
@ -152,7 +186,7 @@ wshack = _{ (wsnc | (comment ~ &(ws* ~ Expr)))* }
|
|||||||
Expr = { prefix* ~ ws* ~ Term ~ ws* ~ postfix* ~ (ws* ~ infix ~ ws* ~ prefix* ~ ws* ~ Term ~ wshack ~ postfix*)* }
|
Expr = { prefix* ~ ws* ~ Term ~ ws* ~ postfix* ~ (ws* ~ infix ~ ws* ~ prefix* ~ ws* ~ Term ~ wshack ~ postfix*)* }
|
||||||
|
|
||||||
// INFIX
|
// INFIX
|
||||||
infix = _{ add | sub | mul | div | pow | modulo | listAdd | equals | band | bor }
|
infix = _{ add | sub | mul | div | pow | modulo | listAdd | equals | notEquals | greaterThanOrEqual | lessThanOrEqual | greaterThan | lessThan | band | bor }
|
||||||
add = { "+" }
|
add = { "+" }
|
||||||
sub = { "-" }
|
sub = { "-" }
|
||||||
mul = { "*" }
|
mul = { "*" }
|
||||||
@ -161,6 +195,11 @@ pow = { "^" }
|
|||||||
modulo = { "%" }
|
modulo = { "%" }
|
||||||
listAdd = { "++" }
|
listAdd = { "++" }
|
||||||
equals = { "==" }
|
equals = { "==" }
|
||||||
|
notEquals = { "!=" }
|
||||||
|
greaterThan = { ">" }
|
||||||
|
lessThan = { "<" }
|
||||||
|
greaterThanOrEqual = { ">=" }
|
||||||
|
lessThanOrEqual = { "<=" }
|
||||||
|
|
||||||
// BUG: these should have forced whitespace at the start!
|
// BUG: these should have forced whitespace at the start!
|
||||||
// (lookbehind wouldn't be the worst feature in the world for a parser grammar!)
|
// (lookbehind wouldn't be the worst feature in the world for a parser grammar!)
|
||||||
@ -174,40 +213,52 @@ bnot = { "not" ~ ws+ }
|
|||||||
|
|
||||||
// POSTFIX
|
// POSTFIX
|
||||||
postfix = _{ unwrap | MethodCall | FieldLookup | Indexing }
|
postfix = _{ unwrap | MethodCall | FieldLookup | Indexing }
|
||||||
unwrap = { "?" } // Not sure I'm convinced about this one, but we can think about it.
|
unwrap = { "!" } // Not sure I'm convinced about this one, but we can think about it.
|
||||||
// Note that functions aren't first-class; we don't allow you to 'call' an arbitrary term.
|
// Note that functions aren't first-class; we don't allow you to 'call' an arbitrary term.
|
||||||
// This is probably for the best since we might have multiple backends for the templating.
|
// This is probably for the best since we might have multiple backends for the templating.
|
||||||
MethodCall = { "." ~ ws* ~ Identifier ~ "(" ~ commaSeparatedExprs ~ ")" }
|
MethodCall = { "." ~ ws* ~ Identifier ~ "(" ~ commaSeparatedExprs ~ ")" }
|
||||||
FieldLookup = { "." ~ ws* ~ Identifier }
|
FieldLookup = { "." ~ ws* ~ Identifier }
|
||||||
Indexing = { "[" ~ ws* ~ Expr ~ ws* ~ "]" }
|
Indexing = { "[" ~ ws* ~ Expr ~ ws* ~ "]" }
|
||||||
|
|
||||||
Term = _{ (IntLiteral | bracketedTerm | FunctionCall | ListLiteral | MapLiteral | String | Variable) }
|
Term = _{ (IntLiteral | bracketedTerm | FunctionCall | ListLiteral | MapLiteral | String | Variable | NoneLiteral | TrueLiteral | FalseLiteral) }
|
||||||
|
|
||||||
bracketedTerm = _{ "(" ~ Expr ~ ")" }
|
bracketedTerm = _{ "(" ~ Expr ~ ")" }
|
||||||
|
|
||||||
|
NoneLiteral = { "None" }
|
||||||
|
TrueLiteral = { "true" }
|
||||||
|
FalseLiteral = { "false" }
|
||||||
IntLiteral = @{ (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT+ | ASCII_DIGIT) }
|
IntLiteral = @{ (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT+ | ASCII_DIGIT) }
|
||||||
Identifier = { (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* }
|
// `-` is important in identifiers for `kebab-case` HTML element attributes
|
||||||
|
// We could consider splitting this out into its own kind of identifier but let's not bother now.
|
||||||
|
Identifier = { (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | "-")* }
|
||||||
commaSeparatedExprs = _{ wsnl* ~ (Expr ~ wsnl* ~ ("," ~ wsnl* ~ Expr ~ wsnl*)* ~ ("," ~ wsnl*)?)? }
|
commaSeparatedExprs = _{ wsnl* ~ (Expr ~ wsnl* ~ ("," ~ wsnl* ~ Expr ~ wsnl*)* ~ ("," ~ wsnl*)?)? }
|
||||||
FunctionCall = { Identifier ~ "(" ~ commaSeparatedExprs ~ ")" }
|
FunctionCall = { Identifier ~ "(" ~ commaSeparatedExprs ~ ")" }
|
||||||
Variable = { "$" ~ Identifier }
|
Variable = { "$" ~ Identifier }
|
||||||
|
|
||||||
ListLiteral = { "[" ~ commaSeparatedExprs ~ "]" }
|
ListLiteral = { "[" ~ commaSeparatedExprs ~ "]" }
|
||||||
|
|
||||||
|
// Basic key-value pairs forming a map literal {a = .., b = ..}.
|
||||||
KVPair = { Identifier ~ wsnl* ~ "=" ~ wsnl* ~ Expr }
|
KVPair = { Identifier ~ wsnl* ~ "=" ~ wsnl* ~ Expr }
|
||||||
KVarShorthand = { Variable }
|
KVarShorthand = { Variable }
|
||||||
commaSeparatedKVPairs = _{ wsnl* ~ ((KVPair | KVarShorthand) ~ wsnl* ~ ("," ~ wsnl* ~ (KVPair | KVarShorthand) ~ wsnl*)* ~ ("," ~ wsnl*)?)? }
|
commaSeparatedKVPairs = _{ wsnl* ~ ((KVPair | KVarShorthand) ~ wsnl* ~ ("," ~ wsnl* ~ (KVPair | KVarShorthand) ~ wsnl*)* ~ ("," ~ wsnl*)?)? }
|
||||||
MapLiteral = { "{" ~ commaSeparatedKVPairs ~ "}" }
|
MapLiteral = { "{" ~ commaSeparatedKVPairs ~ "}" }
|
||||||
|
|
||||||
|
// Element attribute key-value pairs
|
||||||
|
// These can have extra features not found in the basic map literals
|
||||||
|
AttrKVPairOptionalMarker = { "?" }
|
||||||
|
AttrKVPair = { Identifier ~ AttrKVPairOptionalMarker? ~ wsnl* ~ "=" ~ wsnl* ~ Expr }
|
||||||
|
AttrKVarShorthand = { Variable ~ AttrKVPairOptionalMarker? }
|
||||||
|
commaSeparatedAttrKVPairs = _{ wsnl* ~ ((AttrKVPair | AttrKVarShorthand) ~ wsnl* ~ ("," ~ wsnl* ~ (AttrKVPair | AttrKVarShorthand) ~ wsnl*)* ~ ("," ~ wsnl*)?)? }
|
||||||
|
AttrMapLiteral = { "{" ~ commaSeparatedAttrKVPairs ~ "}" }
|
||||||
|
|
||||||
|
|
||||||
// As in a let binding or for binding.
|
// As in a let binding or for binding.
|
||||||
// More options in the future, but for now you just get one identifier and that's it!
|
// More options in the future, but for now you just get one identifier and that's it!
|
||||||
Binding = {
|
Binding = {
|
||||||
VarBinding | IgnoreBinding
|
VarBinding | TupleBinding | IgnoreBinding
|
||||||
}
|
}
|
||||||
VarBinding = { "$" ~ Identifier }
|
VarBinding = { "$" ~ Identifier }
|
||||||
|
TupleBinding = { "(" ~ Binding ~ "," ~ ws* ~ (Binding ~ "," ~ ws*)+ ~ (Binding)? ~ ")" }
|
||||||
IgnoreBinding = { "_" }
|
IgnoreBinding = { "_" }
|
||||||
|
|
||||||
LocalisationIdentifier = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_" | "-")+ }
|
LocalisationIdentifier = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_" | "-")+ }
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case, clippy::result_large_err)]
|
||||||
|
|
||||||
use crate::ast::{
|
use crate::ast::{
|
||||||
Binding, Block, ComponentElement, DefineExpandSlot, DefineFragment, Expression, ForBlock,
|
Binding, Block, ComponentElement, DefineExpandSlot, DefineFragment, ElementAttributeFlags,
|
||||||
HtmlElement, IfBlock, MatchBinding, MatchBlock, StringExpr, StringPiece, Template,
|
Expression, ForBlock, HtmlElement, IfBlock, MatchBinding, MatchBlock, ParameterDefinition,
|
||||||
|
SetStatement, StringExpr, StringPiece, Template,
|
||||||
};
|
};
|
||||||
use crate::{intern, IStr, Locator};
|
use crate::{intern, IStr, Locator};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
@ -42,7 +43,12 @@ fn error<R: Copy + Debug + Hash + Ord>(msg: &str, span: Span) -> PCError<R> {
|
|||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref PRATT_PARSER: PrattParser<Rule> = PrattParser::new()
|
static ref PRATT_PARSER: PrattParser<Rule> = PrattParser::new()
|
||||||
.op(Op::infix(Rule::band, Assoc::Left) | Op::infix(Rule::bor, Assoc::Left))
|
.op(Op::infix(Rule::band, Assoc::Left) | Op::infix(Rule::bor, Assoc::Left))
|
||||||
.op(Op::infix(Rule::equals, Assoc::Left))
|
.op(Op::infix(Rule::equals, Assoc::Left)
|
||||||
|
| Op::infix(Rule::notEquals, Assoc::Left)
|
||||||
|
| Op::infix(Rule::lessThan, Assoc::Left)
|
||||||
|
| Op::infix(Rule::greaterThan, Assoc::Left)
|
||||||
|
| Op::infix(Rule::greaterThanOrEqual, Assoc::Left)
|
||||||
|
| Op::infix(Rule::lessThanOrEqual, Assoc::Left))
|
||||||
.op(Op::infix(Rule::add, Assoc::Left) | Op::infix(Rule::sub, Assoc::Left))
|
.op(Op::infix(Rule::add, Assoc::Left) | Op::infix(Rule::sub, Assoc::Left))
|
||||||
.op(Op::infix(Rule::mul, Assoc::Left) | Op::infix(Rule::div, Assoc::Left))
|
.op(Op::infix(Rule::mul, Assoc::Left) | Op::infix(Rule::div, Assoc::Left))
|
||||||
.op(Op::infix(Rule::pow, Assoc::Right))
|
.op(Op::infix(Rule::pow, Assoc::Right))
|
||||||
@ -55,8 +61,51 @@ lazy_static! {
|
|||||||
#[pest_consume::parser]
|
#[pest_consume::parser]
|
||||||
impl HornbeamParser {
|
impl HornbeamParser {
|
||||||
fn Hornbeam(input: Node) -> PCResult<Template> {
|
fn Hornbeam(input: Node) -> PCResult<Template> {
|
||||||
let blocks = HornbeamParser::helper_blocks(input.into_children())?;
|
Ok(match_nodes!(input.into_children();
|
||||||
Ok(Template { blocks })
|
[PreambleList(param_defs), HornbeamBlockList(blocks), EOI(_)] => {
|
||||||
|
Template { param_defs, blocks }
|
||||||
|
},
|
||||||
|
[HornbeamBlockList(blocks), EOI(_)] => {
|
||||||
|
Template { param_defs: None, blocks }
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn PreambleList(input: Node) -> PCResult<Option<Vec<ParameterDefinition>>> {
|
||||||
|
let param_defs: Vec<ParameterDefinition> = match_nodes!(input.into_children();
|
||||||
|
[ParameterDefinition(param_defs)..] => param_defs.collect()
|
||||||
|
);
|
||||||
|
// Templates with no parameter definitions are treated as untyped.
|
||||||
|
// (Mostly for backwards compat)
|
||||||
|
Ok(if param_defs.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(param_defs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ParameterDefinition(input: Node) -> PCResult<ParameterDefinition> {
|
||||||
|
let loc = nodeloc(&input);
|
||||||
|
Ok(match_nodes!(input.into_children();
|
||||||
|
[Identifier(name), Expr(default)] => {
|
||||||
|
ParameterDefinition {
|
||||||
|
name,
|
||||||
|
loc,
|
||||||
|
default: Some(default),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[Identifier(name)] => {
|
||||||
|
ParameterDefinition {
|
||||||
|
name,
|
||||||
|
loc,
|
||||||
|
default: None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn HornbeamBlockList(input: Node) -> PCResult<Vec<Block>> {
|
||||||
|
HornbeamParser::helper_blocks(input.into_children())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn Element(input: Node) -> PCResult<Block> {
|
fn Element(input: Node) -> PCResult<Block> {
|
||||||
@ -72,7 +121,7 @@ impl HornbeamParser {
|
|||||||
let mut supply_slots = Vec::new();
|
let mut supply_slots = Vec::new();
|
||||||
let mut blocks = Vec::new();
|
let mut blocks = Vec::new();
|
||||||
|
|
||||||
while let Some(next) = children.next() {
|
for next in children {
|
||||||
match next.as_rule() {
|
match next.as_rule() {
|
||||||
Rule::CssClass => {
|
Rule::CssClass => {
|
||||||
classes.push(HornbeamParser::CssClass(next)?);
|
classes.push(HornbeamParser::CssClass(next)?);
|
||||||
@ -83,8 +132,8 @@ impl HornbeamParser {
|
|||||||
Rule::SupplySlot => {
|
Rule::SupplySlot => {
|
||||||
supply_slots.push(HornbeamParser::SupplySlot(next)?);
|
supply_slots.push(HornbeamParser::SupplySlot(next)?);
|
||||||
}
|
}
|
||||||
Rule::MapLiteral => {
|
Rule::AttrMapLiteral => {
|
||||||
attributes = HornbeamParser::MapLiteral(next)?;
|
attributes = HornbeamParser::AttrMapLiteral(next)?;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(block) = HornbeamParser::helper_block(next)? {
|
if let Some(block) = HornbeamParser::helper_block(next)? {
|
||||||
@ -108,6 +157,20 @@ impl HornbeamParser {
|
|||||||
loc,
|
loc,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
let attributes: PCResult<BTreeMap<IStr, Expression>> = attributes
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, (v, attrs))| {
|
||||||
|
if attrs.optional {
|
||||||
|
Err(error(
|
||||||
|
"Optional arguments to components are currently unsupported",
|
||||||
|
span,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok((k, v))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let attributes = attributes?;
|
||||||
if !supply_slots.is_empty() {
|
if !supply_slots.is_empty() {
|
||||||
let mut slots = BTreeMap::new();
|
let mut slots = BTreeMap::new();
|
||||||
for (slot_name, slot_content_blocks, _slot_span) in supply_slots {
|
for (slot_name, slot_content_blocks, _slot_span) in supply_slots {
|
||||||
@ -197,11 +260,26 @@ impl HornbeamParser {
|
|||||||
)?))
|
)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn RawUnescapedHtml(input: Node) -> PCResult<Block> {
|
||||||
|
Ok(Block::RawUnescapedHtml(HornbeamParser::String(
|
||||||
|
input.into_children().single()?,
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
|
||||||
fn String(input: Node) -> PCResult<StringExpr> {
|
fn String(input: Node) -> PCResult<StringExpr> {
|
||||||
let mut pieces = Vec::new();
|
let mut pieces = Vec::new();
|
||||||
for node in input.into_children() {
|
for node in input.into_children() {
|
||||||
|
let loc = nodeloc(&node);
|
||||||
match node.as_rule() {
|
match node.as_rule() {
|
||||||
Rule::SEscape => pieces.push(StringPiece::Literal(HornbeamParser::SEscape(node)?)),
|
Rule::SEscape => pieces.push(StringPiece::Literal(HornbeamParser::SEscape(node)?)),
|
||||||
|
Rule::SSimpleVarInterpol => {
|
||||||
|
let var_identifier =
|
||||||
|
HornbeamParser::Identifier(node.into_children().single()?)?;
|
||||||
|
pieces.push(StringPiece::Interpolation(Expression::Variable {
|
||||||
|
name: var_identifier,
|
||||||
|
loc,
|
||||||
|
}));
|
||||||
|
}
|
||||||
Rule::SInterpol => pieces.push(StringPiece::Interpolation(HornbeamParser::Expr(
|
Rule::SInterpol => pieces.push(StringPiece::Interpolation(HornbeamParser::Expr(
|
||||||
node.into_children().single()?,
|
node.into_children().single()?,
|
||||||
)?)),
|
)?)),
|
||||||
@ -258,6 +336,52 @@ impl HornbeamParser {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn AttrKVPair(input: Node) -> PCResult<(IStr, (Expression, ElementAttributeFlags))> {
|
||||||
|
Ok(match_nodes!(input.into_children();
|
||||||
|
[Identifier(key), Expr(value)] => (key, (value, ElementAttributeFlags {
|
||||||
|
optional: false
|
||||||
|
})),
|
||||||
|
[Identifier(key), AttrKVPairOptionalMarker(_), Expr(value)] => (key, (value, ElementAttributeFlags {
|
||||||
|
optional: true
|
||||||
|
})),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn AttrKVPairOptionalMarker(input: Node) -> PCResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn AttrKVarShorthand(input: Node) -> PCResult<(IStr, (Expression, ElementAttributeFlags))> {
|
||||||
|
let (var_expr, optional) = match_nodes!(input.into_children();
|
||||||
|
[Variable(var_expr), AttrKVPairOptionalMarker(_)] => {
|
||||||
|
(var_expr, true)
|
||||||
|
},
|
||||||
|
[Variable(var_expr)] => {
|
||||||
|
(var_expr, false)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if let Expression::Variable { name, .. } = &var_expr {
|
||||||
|
Ok((name.clone(), (var_expr, ElementAttributeFlags { optional })))
|
||||||
|
} else {
|
||||||
|
unreachable!("Variable should also be returned from Variable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn AttrMapLiteral(
|
||||||
|
input: Node,
|
||||||
|
) -> PCResult<BTreeMap<IStr, (Expression, ElementAttributeFlags)>> {
|
||||||
|
input
|
||||||
|
.into_children()
|
||||||
|
.map(|node| match node.as_rule() {
|
||||||
|
Rule::AttrKVPair => HornbeamParser::AttrKVPair(node),
|
||||||
|
Rule::AttrKVarShorthand => HornbeamParser::AttrKVarShorthand(node),
|
||||||
|
other => {
|
||||||
|
unimplemented!("unexpected {other:?} in AttrMapLiteral");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn SEscape(input: Node) -> PCResult<IStr> {
|
fn SEscape(input: Node) -> PCResult<IStr> {
|
||||||
let esc = input.as_str();
|
let esc = input.as_str();
|
||||||
Ok(match esc {
|
Ok(match esc {
|
||||||
@ -279,6 +403,9 @@ impl HornbeamParser {
|
|||||||
let node = Node::new_with_user_data(primary, ud.clone());
|
let node = Node::new_with_user_data(primary, ud.clone());
|
||||||
Ok(match node.as_rule() {
|
Ok(match node.as_rule() {
|
||||||
Rule::IntLiteral => Expression::IntLiteral { val: node.as_str().parse().map_err(|e| error(&format!("can't parse int: {e:?}"), node.as_span()))? },
|
Rule::IntLiteral => Expression::IntLiteral { val: node.as_str().parse().map_err(|e| error(&format!("can't parse int: {e:?}"), node.as_span()))? },
|
||||||
|
Rule::NoneLiteral => Expression::NoneLiteral,
|
||||||
|
Rule::TrueLiteral => Expression::BoolLiteral(true),
|
||||||
|
Rule::FalseLiteral => Expression::BoolLiteral(false),
|
||||||
Rule::String => Expression::StringExpr(HornbeamParser::String(node)?),
|
Rule::String => Expression::StringExpr(HornbeamParser::String(node)?),
|
||||||
Rule::Variable => HornbeamParser::Variable(node)?,
|
Rule::Variable => HornbeamParser::Variable(node)?,
|
||||||
Rule::FunctionCall => HornbeamParser::FunctionCall(node)?,
|
Rule::FunctionCall => HornbeamParser::FunctionCall(node)?,
|
||||||
@ -294,6 +421,11 @@ impl HornbeamParser {
|
|||||||
Rule::mul => Expression::Mul { left: Box::new(lhs?), right: Box::new(rhs?) },
|
Rule::mul => Expression::Mul { left: Box::new(lhs?), right: Box::new(rhs?) },
|
||||||
Rule::div => Expression::Div { left: Box::new(lhs?), right: Box::new(rhs?) },
|
Rule::div => Expression::Div { left: Box::new(lhs?), right: Box::new(rhs?) },
|
||||||
Rule::equals => Expression::Equals { left: Box::new(lhs?), right: Box::new(rhs?) },
|
Rule::equals => Expression::Equals { left: Box::new(lhs?), right: Box::new(rhs?) },
|
||||||
|
Rule::notEquals => Expression::BNot { sub: Box::new(Expression::Equals { left: Box::new(lhs?), right: Box::new(rhs?) }) },
|
||||||
|
Rule::lessThan => Expression::LessThan { left: Box::new(lhs?), right: Box::new(rhs?) },
|
||||||
|
Rule::lessThanOrEqual => Expression::LessThanOrEquals { left: Box::new(lhs?), right: Box::new(rhs?) },
|
||||||
|
Rule::greaterThan => Expression::BNot { sub: Box::new(Expression::LessThanOrEquals { left: Box::new(lhs?), right: Box::new(rhs?) }) },
|
||||||
|
Rule::greaterThanOrEqual => Expression::BNot { sub: Box::new(Expression::LessThan { left: Box::new(lhs?), right: Box::new(rhs?) }) },
|
||||||
Rule::bor => Expression::BOr { left: Box::new(lhs?), right: Box::new(rhs?) },
|
Rule::bor => Expression::BOr { left: Box::new(lhs?), right: Box::new(rhs?) },
|
||||||
Rule::band => Expression::BAnd { left: Box::new(lhs?), right: Box::new(rhs?) },
|
Rule::band => Expression::BAnd { left: Box::new(lhs?), right: Box::new(rhs?) },
|
||||||
other => unimplemented!("unimp infix {other:?}!"),
|
other => unimplemented!("unimp infix {other:?}!"),
|
||||||
@ -302,7 +434,9 @@ impl HornbeamParser {
|
|||||||
let node = Node::new_with_user_data(op, ud.clone());
|
let node = Node::new_with_user_data(op, ud.clone());
|
||||||
let loc = nodeloc(&node);
|
let loc = nodeloc(&node);
|
||||||
Ok(match node.as_rule() {
|
Ok(match node.as_rule() {
|
||||||
Rule::unwrap => unimplemented!("unimp unwrap"),
|
Rule::unwrap => {
|
||||||
|
Expression::Unwrap { obj: Box::new(lhs?), loc }
|
||||||
|
},
|
||||||
Rule::FieldLookup => {
|
Rule::FieldLookup => {
|
||||||
let ident = intern(node.into_children().single()?.as_str());
|
let ident = intern(node.into_children().single()?.as_str());
|
||||||
Expression::FieldLookup { obj: Box::new(lhs?), ident, loc }
|
Expression::FieldLookup { obj: Box::new(lhs?), ident, loc }
|
||||||
@ -329,6 +463,22 @@ impl HornbeamParser {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn SetStatement(input: Node) -> PCResult<Block> {
|
||||||
|
let loc = nodeloc(&input);
|
||||||
|
|
||||||
|
let (binding, expression) = match_nodes!(input.into_children();
|
||||||
|
[Binding(binding), Expr(expression)] => {
|
||||||
|
(binding, expression)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Block::SetStatement(SetStatement {
|
||||||
|
binding,
|
||||||
|
expression,
|
||||||
|
loc,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
fn IfCondition(input: Node) -> PCResult<Expression> {
|
fn IfCondition(input: Node) -> PCResult<Expression> {
|
||||||
Self::Expr(input.into_children().single()?)
|
Self::Expr(input.into_children().single()?)
|
||||||
}
|
}
|
||||||
@ -422,6 +572,7 @@ impl HornbeamParser {
|
|||||||
Ok(match_nodes!(input.into_children();
|
Ok(match_nodes!(input.into_children();
|
||||||
[VarBinding(b)] => b,
|
[VarBinding(b)] => b,
|
||||||
[IgnoreBinding(b)] => b,
|
[IgnoreBinding(b)] => b,
|
||||||
|
[TupleBinding(b)] => b,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,6 +585,14 @@ impl HornbeamParser {
|
|||||||
Ok(Binding::Ignore)
|
Ok(Binding::Ignore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn TupleBinding(input: Node) -> PCResult<Binding> {
|
||||||
|
Ok(match_nodes!(input.into_children();
|
||||||
|
[Binding(bs)..] => {
|
||||||
|
Binding::Tuple(bs.collect())
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn EmptyForBlock(input: Node) -> PCResult<Vec<Block>> {
|
fn EmptyForBlock(input: Node) -> PCResult<Vec<Block>> {
|
||||||
HornbeamParser::helper_blocks(input.into_children())
|
HornbeamParser::helper_blocks(input.into_children())
|
||||||
}
|
}
|
||||||
@ -459,10 +618,12 @@ impl HornbeamParser {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn helper_block<'a>(input: Node) -> PCResult<Option<Block>> {
|
fn helper_block(input: Node) -> PCResult<Option<Block>> {
|
||||||
Ok(match input.as_rule() {
|
Ok(match input.as_rule() {
|
||||||
Rule::Element => Some(HornbeamParser::Element(input)?),
|
Rule::Element => Some(HornbeamParser::Element(input)?),
|
||||||
Rule::Text => Some(HornbeamParser::Text(input)?),
|
Rule::Text => Some(HornbeamParser::Text(input)?),
|
||||||
|
Rule::RawUnescapedHtml => Some(HornbeamParser::RawUnescapedHtml(input)?),
|
||||||
|
Rule::SetStatement => Some(HornbeamParser::SetStatement(input)?),
|
||||||
Rule::IfBlock => Some(HornbeamParser::IfBlock(input)?),
|
Rule::IfBlock => Some(HornbeamParser::IfBlock(input)?),
|
||||||
Rule::ForBlock => Some(HornbeamParser::ForBlock(input)?),
|
Rule::ForBlock => Some(HornbeamParser::ForBlock(input)?),
|
||||||
Rule::MatchBlock => Some(HornbeamParser::MatchBlock(input)?),
|
Rule::MatchBlock => Some(HornbeamParser::MatchBlock(input)?),
|
||||||
@ -503,6 +664,23 @@ div
|
|||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn param_defs() {
|
||||||
|
assert_yaml_snapshot!(parse_template(
|
||||||
|
r#"
|
||||||
|
declare
|
||||||
|
// We need form information
|
||||||
|
param $form
|
||||||
|
|
||||||
|
// Also wouldn't hurt to have a default value on this other parameter
|
||||||
|
|
||||||
|
param $user = None
|
||||||
|
"#,
|
||||||
|
"inp"
|
||||||
|
)
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn supply_slots_to_components_only() {
|
fn supply_slots_to_components_only() {
|
||||||
assert_debug_snapshot!(parse_template(
|
assert_debug_snapshot!(parse_template(
|
||||||
@ -638,4 +816,17 @@ div
|
|||||||
)
|
)
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn raw() {
|
||||||
|
assert_yaml_snapshot!(parse_template(
|
||||||
|
r#"
|
||||||
|
div
|
||||||
|
span
|
||||||
|
raw "<u>wow $x ${$x} @wowage{}</u>"
|
||||||
|
"#,
|
||||||
|
"inp"
|
||||||
|
)
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
source: hornbeam_grammar/src/parser.rs
|
source: hornbeam_grammar/src/parser.rs
|
||||||
expression: "parse_template(r#\"\nfor $x in $xs\n for $y in $ys\n \"Woot\"\n empty\n \"no ys\"\nempty\n \"no xs\"\n \"#,\n \"inp\").unwrap()"
|
expression: "parse_template(r#\"\nfor $x in $xs\n for $y in $ys\n \"Woot\"\n empty\n \"no ys\"\nempty\n \"no xs\"\n \"#,\n \"inp\").unwrap()"
|
||||||
---
|
---
|
||||||
|
param_defs: ~
|
||||||
blocks:
|
blocks:
|
||||||
- ForBlock:
|
- ForBlock:
|
||||||
binding:
|
binding:
|
||||||
@ -44,4 +45,3 @@ blocks:
|
|||||||
filename: inp
|
filename: inp
|
||||||
line: 2
|
line: 2
|
||||||
column: 1
|
column: 1
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
source: hornbeam_grammar/src/parser.rs
|
source: hornbeam_grammar/src/parser.rs
|
||||||
expression: "parse_template(r#\"\ndiv\n fragment BobbyDazzler\n span\n \"Wow!\"\n fragment MainPart\n slot :main\n div\n optional slot :footer\n \"#,\n \"inp\").unwrap()"
|
expression: "parse_template(r#\"\ndiv\n fragment BobbyDazzler\n span\n \"Wow!\"\n fragment MainPart\n slot :main\n div\n optional slot :footer\n \"#,\n \"inp\").unwrap()"
|
||||||
---
|
---
|
||||||
|
param_defs: ~
|
||||||
blocks:
|
blocks:
|
||||||
- HtmlElement:
|
- HtmlElement:
|
||||||
name: div
|
name: div
|
||||||
@ -64,4 +65,3 @@ blocks:
|
|||||||
filename: inp
|
filename: inp
|
||||||
line: 2
|
line: 2
|
||||||
column: 1
|
column: 1
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
source: hornbeam_grammar/src/parser.rs
|
source: hornbeam_grammar/src/parser.rs
|
||||||
expression: "parse_template(r#\"\nif 1 + 1 == 2\n div\n \"Phew, safe!\"\nelse if 1 + 2 - 1 == 3 // not too far off, I suppose\n Warning\n \"Not quite, but fairly close. What kind of world is this?\"\nelse // peculiar.\n \"Not even close, eh?\"\n \"#,\n \"inp\").unwrap()"
|
expression: "parse_template(r#\"\nif 1 + 1 == 2\n div\n \"Phew, safe!\"\nelse if 1 + 2 - 1 == 3 // not too far off, I suppose\n Warning\n \"Not quite, but fairly close. What kind of world is this?\"\nelse // peculiar.\n \"Not even close, eh?\"\n \"#,\n \"inp\").unwrap()"
|
||||||
---
|
---
|
||||||
|
param_defs: ~
|
||||||
blocks:
|
blocks:
|
||||||
- IfBlock:
|
- IfBlock:
|
||||||
condition:
|
condition:
|
||||||
@ -76,4 +77,3 @@ blocks:
|
|||||||
filename: inp
|
filename: inp
|
||||||
line: 2
|
line: 2
|
||||||
column: 1
|
column: 1
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
source: hornbeam_grammar/src/parser.rs
|
source: hornbeam_grammar/src/parser.rs
|
||||||
expression: "parse_template(r#\"\nif 10 / 2 == 5 or $point.x == 42 // div for a div?\n div\n \"#,\n \"inp\").unwrap()"
|
expression: "parse_template(r#\"\nif 10 / 2 == 5 or $point.x == 42 // div for a div?\n div\n \"#,\n \"inp\").unwrap()"
|
||||||
---
|
---
|
||||||
|
param_defs: ~
|
||||||
blocks:
|
blocks:
|
||||||
- IfBlock:
|
- IfBlock:
|
||||||
condition:
|
condition:
|
||||||
@ -54,4 +55,3 @@ blocks:
|
|||||||
filename: inp
|
filename: inp
|
||||||
line: 2
|
line: 2
|
||||||
column: 1
|
column: 1
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
source: hornbeam_grammar/src/parser.rs
|
source: hornbeam_grammar/src/parser.rs
|
||||||
expression: "parse_template(r#\"\ndiv\n span\n @header-wow\n MainBody\n ''\n @body-welcome\n @body-msg{count = $messages.len()}\n ''\n Footer\n @footer-copyright{year = today().year}\n \"#,\n \"inp\").unwrap()"
|
expression: "parse_template(r#\"\ndiv\n span\n @header-wow\n MainBody\n ''\n @body-welcome\n @body-msg{count = $messages.len()}\n ''\n Footer\n @footer-copyright{year = today().year}\n \"#,\n \"inp\").unwrap()"
|
||||||
---
|
---
|
||||||
|
param_defs: ~
|
||||||
blocks:
|
blocks:
|
||||||
- HtmlElement:
|
- HtmlElement:
|
||||||
name: div
|
name: div
|
||||||
@ -90,4 +91,3 @@ blocks:
|
|||||||
filename: inp
|
filename: inp
|
||||||
line: 2
|
line: 2
|
||||||
column: 1
|
column: 1
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
source: hornbeam_grammar/src/parser.rs
|
source: hornbeam_grammar/src/parser.rs
|
||||||
expression: "parse_template(r#\"\nif $x\n match $y\n None =>\n \"None\"\n Some($z) =>\n \"Some(${$z})\"\n \"#,\n \"inp\").unwrap()"
|
expression: "parse_template(r#\"\nif $x\n match $y\n None =>\n \"None\"\n Some($z) =>\n \"Some(${$z})\"\n \"#,\n \"inp\").unwrap()"
|
||||||
---
|
---
|
||||||
|
param_defs: ~
|
||||||
blocks:
|
blocks:
|
||||||
- IfBlock:
|
- IfBlock:
|
||||||
condition:
|
condition:
|
||||||
@ -50,4 +51,3 @@ blocks:
|
|||||||
filename: inp
|
filename: inp
|
||||||
line: 2
|
line: 2
|
||||||
column: 1
|
column: 1
|
||||||
|
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
source: hornbeam_grammar/src/parser.rs
|
||||||
|
expression: "parse_template(r#\"\ndeclare\n // We need form information\n param $form\n\n // Also wouldn't hurt to have a default value on this other parameter\n\n param $user = None\n \"#,\n \"inp\").unwrap()"
|
||||||
|
---
|
||||||
|
param_defs:
|
||||||
|
- name: form
|
||||||
|
loc:
|
||||||
|
filename: inp
|
||||||
|
line: 4
|
||||||
|
column: 5
|
||||||
|
default: ~
|
||||||
|
- name: user
|
||||||
|
loc:
|
||||||
|
filename: inp
|
||||||
|
line: 8
|
||||||
|
column: 5
|
||||||
|
default: NoneLiteral
|
||||||
|
blocks: []
|
@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
source: hornbeam_grammar/src/parser.rs
|
||||||
|
expression: "parse_template(r#\"\ndiv\n span\n raw \"<u>wow $x ${$x} @wowage{}</u>\"\n \"#,\n \"inp\").unwrap()"
|
||||||
|
---
|
||||||
|
param_defs: ~
|
||||||
|
blocks:
|
||||||
|
- HtmlElement:
|
||||||
|
name: div
|
||||||
|
children:
|
||||||
|
- HtmlElement:
|
||||||
|
name: span
|
||||||
|
children:
|
||||||
|
- RawUnescapedHtml:
|
||||||
|
pieces:
|
||||||
|
- Literal: "<u>wow "
|
||||||
|
- Interpolation:
|
||||||
|
Variable:
|
||||||
|
name: x
|
||||||
|
loc:
|
||||||
|
filename: inp
|
||||||
|
line: 4
|
||||||
|
column: 21
|
||||||
|
- Literal: " "
|
||||||
|
- Interpolation:
|
||||||
|
Variable:
|
||||||
|
name: x
|
||||||
|
loc:
|
||||||
|
filename: inp
|
||||||
|
line: 4
|
||||||
|
column: 26
|
||||||
|
- Literal: " @wowage{}</u>"
|
||||||
|
classes: []
|
||||||
|
dom_id: ~
|
||||||
|
attributes: {}
|
||||||
|
loc:
|
||||||
|
filename: inp
|
||||||
|
line: 3
|
||||||
|
column: 5
|
||||||
|
classes: []
|
||||||
|
dom_id: ~
|
||||||
|
attributes: {}
|
||||||
|
loc:
|
||||||
|
filename: inp
|
||||||
|
line: 2
|
||||||
|
column: 1
|
@ -2,6 +2,7 @@
|
|||||||
source: hornbeam_grammar/src/parser.rs
|
source: hornbeam_grammar/src/parser.rs
|
||||||
expression: "parse_template(r#\"\n// This is a simple Hornbeam template that just shows a <div>\ndiv\n \"#,\n \"inp\").unwrap()"
|
expression: "parse_template(r#\"\n// This is a simple Hornbeam template that just shows a <div>\ndiv\n \"#,\n \"inp\").unwrap()"
|
||||||
---
|
---
|
||||||
|
param_defs: ~
|
||||||
blocks:
|
blocks:
|
||||||
- HtmlElement:
|
- HtmlElement:
|
||||||
name: div
|
name: div
|
||||||
@ -13,4 +14,3 @@ blocks:
|
|||||||
filename: inp
|
filename: inp
|
||||||
line: 3
|
line: 3
|
||||||
column: 1
|
column: 1
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
source: hornbeam_grammar/src/parser.rs
|
source: hornbeam_grammar/src/parser.rs
|
||||||
expression: "parse_template(r#\"\nMyComponent\n :someslot\n ''\n ${\"abc\" + \"def${ 1 + 1 }\"}\n Not too bad now.\n ''\n \"#,\n \"inp\").unwrap()"
|
expression: "parse_template(r#\"\nMyComponent\n :someslot\n ''\n ${\"abc\" + \"def${ 1 + 1 }\"}\n Not too bad now.\n ''\n \"#,\n \"inp\").unwrap()"
|
||||||
---
|
---
|
||||||
|
param_defs: ~
|
||||||
blocks:
|
blocks:
|
||||||
- ComponentElement:
|
- ComponentElement:
|
||||||
name: MyComponent
|
name: MyComponent
|
||||||
@ -34,4 +35,3 @@ blocks:
|
|||||||
filename: inp
|
filename: inp
|
||||||
line: 2
|
line: 2
|
||||||
column: 1
|
column: 1
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
source: hornbeam_grammar/src/parser.rs
|
source: hornbeam_grammar/src/parser.rs
|
||||||
expression: "parse_template(r#\"\nMyComponent\n :someslot\n \"That's better!\"\n \"#,\n \"inp\").unwrap()"
|
expression: "parse_template(r#\"\nMyComponent\n :someslot\n \"That's better!\"\n \"#,\n \"inp\").unwrap()"
|
||||||
---
|
---
|
||||||
|
param_defs: ~
|
||||||
blocks:
|
blocks:
|
||||||
- ComponentElement:
|
- ComponentElement:
|
||||||
name: MyComponent
|
name: MyComponent
|
||||||
@ -15,4 +16,3 @@ blocks:
|
|||||||
filename: inp
|
filename: inp
|
||||||
line: 2
|
line: 2
|
||||||
column: 1
|
column: 1
|
||||||
|
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "hornbeam_interpreter"
|
name = "hornbeam_interpreter"
|
||||||
version = "0.0.1"
|
description = "Interpreter for the Hornbeam template language. This is the low-level implementation crate; not advised for direct use in applications."
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
version = "0.0.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hornbeam_grammar = { version = "0.0.1", path = "../hornbeam_grammar" }
|
hornbeam_grammar = { version = "0.0.5", path = "../hornbeam_grammar" }
|
||||||
hornbeam_ir = { version = "0.0.1", path = "../hornbeam_ir" }
|
hornbeam_ir = { version = "0.0.5", path = "../hornbeam_ir" }
|
||||||
|
|
||||||
fluent-templates = { version = "0.8.0", optional = true }
|
fluent-templates = { version = "0.8.0", optional = true }
|
||||||
bevy_reflect = { version = "0.11.0" }
|
bevy_reflect.workspace = true
|
||||||
html-escape = "0.2.13"
|
html-escape = "0.2.13"
|
||||||
|
formbeam = { version = "0.0.5", path = "../formbeam", optional = true }
|
||||||
|
|
||||||
walkdir = "2.3.2"
|
walkdir = "2.3.2"
|
||||||
|
|
||||||
@ -23,7 +26,12 @@ itertools = "0.10.5"
|
|||||||
pollster = "0.3.0"
|
pollster = "0.3.0"
|
||||||
|
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
|
percent-encoding = "2.2.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["fluent"]
|
default = ["fluent"]
|
||||||
fluent = ["fluent-templates"]
|
fluent = ["dep:fluent-templates"]
|
||||||
|
formbeam = ["dep:formbeam"]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
insta = "1.38.0"
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::functions::TemplateAccessibleMethod;
|
||||||
use crate::interface::{LocalisationSystem, OutputSystem};
|
use crate::interface::{LocalisationSystem, OutputSystem};
|
||||||
use crate::InterpreterError;
|
use crate::InterpreterError;
|
||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
@ -5,7 +6,7 @@ use bevy_reflect::{FromReflect, Reflect, ReflectRef, VariantType};
|
|||||||
use fluent_templates::lazy_static::lazy_static;
|
use fluent_templates::lazy_static::lazy_static;
|
||||||
use hornbeam_grammar::ast::{Binding, MatchBinding};
|
use hornbeam_grammar::ast::{Binding, MatchBinding};
|
||||||
use hornbeam_grammar::Locator;
|
use hornbeam_grammar::Locator;
|
||||||
use hornbeam_ir::ir::{Expression, Step, StepDef, StringPiece};
|
use hornbeam_ir::ir::{Expression, Step, StepDef, StringPiece, TemplateFunction};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
@ -27,11 +28,12 @@ pub(crate) struct Scope<'a> {
|
|||||||
|
|
||||||
pub(crate) struct Interpreter<'a, O, LS> {
|
pub(crate) struct Interpreter<'a, O, LS> {
|
||||||
pub(crate) entrypoint: String,
|
pub(crate) entrypoint: String,
|
||||||
pub(crate) program: &'a BTreeMap<String, Arc<Vec<Step>>>,
|
pub(crate) program: &'a BTreeMap<String, Arc<TemplateFunction>>,
|
||||||
pub(crate) output: O,
|
pub(crate) output: O,
|
||||||
pub(crate) localisation: Arc<LS>,
|
pub(crate) localisation: Arc<LS>,
|
||||||
pub(crate) locale: String,
|
pub(crate) locale: String,
|
||||||
pub(crate) scopes: Vec<Scope<'a>>,
|
pub(crate) scopes: Vec<Scope<'a>>,
|
||||||
|
pub(crate) methods: &'a BTreeMap<String, TemplateAccessibleMethod>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -50,7 +52,13 @@ impl Value {
|
|||||||
Value::Int(_) => "Int",
|
Value::Int(_) => "Int",
|
||||||
Value::Bool(_) => "Bool",
|
Value::Bool(_) => "Bool",
|
||||||
Value::List(_) => "List",
|
Value::List(_) => "List",
|
||||||
Value::Reflective(reflective) => reflective.type_name(),
|
// TODO get rid of unwraps
|
||||||
|
Value::Reflective(reflective) => reflective
|
||||||
|
.get_represented_type_info()
|
||||||
|
.unwrap()
|
||||||
|
.type_path_table()
|
||||||
|
.ident()
|
||||||
|
.unwrap(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,6 +67,7 @@ lazy_static! {
|
|||||||
static ref U8_TYPEID: TypeId = TypeId::of::<u8>();
|
static ref U8_TYPEID: TypeId = TypeId::of::<u8>();
|
||||||
static ref U16_TYPEID: TypeId = TypeId::of::<u16>();
|
static ref U16_TYPEID: TypeId = TypeId::of::<u16>();
|
||||||
static ref U32_TYPEID: TypeId = TypeId::of::<u32>();
|
static ref U32_TYPEID: TypeId = TypeId::of::<u32>();
|
||||||
|
static ref U64_TYPEID: TypeId = TypeId::of::<u64>();
|
||||||
static ref I8_TYPEID: TypeId = TypeId::of::<i8>();
|
static ref I8_TYPEID: TypeId = TypeId::of::<i8>();
|
||||||
static ref I16_TYPEID: TypeId = TypeId::of::<i16>();
|
static ref I16_TYPEID: TypeId = TypeId::of::<i16>();
|
||||||
static ref I32_TYPEID: TypeId = TypeId::of::<i32>();
|
static ref I32_TYPEID: TypeId = TypeId::of::<i32>();
|
||||||
@ -80,6 +89,16 @@ impl Value {
|
|||||||
Value::Int(u16::from_reflect(reflect.deref()).unwrap() as i64)
|
Value::Int(u16::from_reflect(reflect.deref()).unwrap() as i64)
|
||||||
} else if ti == *U32_TYPEID {
|
} else if ti == *U32_TYPEID {
|
||||||
Value::Int(u32::from_reflect(reflect.deref()).unwrap() as i64)
|
Value::Int(u32::from_reflect(reflect.deref()).unwrap() as i64)
|
||||||
|
} else if ti == *U64_TYPEID {
|
||||||
|
// If we can, convert it to a native i64 value
|
||||||
|
// If we can't (because it's bigger than i64::MAX),
|
||||||
|
// then we have no choice but to leave it as a reflective value... :/
|
||||||
|
// TODO I really don't like this but my hands are tied...
|
||||||
|
let value_u64 = u64::from_reflect(reflect.deref()).unwrap();
|
||||||
|
match i64::try_from(value_u64) {
|
||||||
|
Ok(value_i64) => Value::Int(value_i64),
|
||||||
|
Err(_cant) => Value::Reflective(reflect),
|
||||||
|
}
|
||||||
} else if ti == *I8_TYPEID {
|
} else if ti == *I8_TYPEID {
|
||||||
Value::Int(i8::from_reflect(reflect.deref()).unwrap() as i64)
|
Value::Int(i8::from_reflect(reflect.deref()).unwrap() as i64)
|
||||||
} else if ti == *I16_TYPEID {
|
} else if ti == *I16_TYPEID {
|
||||||
@ -110,6 +129,69 @@ impl Clone for Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Binder {
|
||||||
|
variables_to_unbind: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Binder {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Binder {
|
||||||
|
variables_to_unbind: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bind(
|
||||||
|
&mut self,
|
||||||
|
variables: &mut BTreeMap<String, Value>,
|
||||||
|
binding: &Binding,
|
||||||
|
value: Value,
|
||||||
|
) {
|
||||||
|
match binding {
|
||||||
|
Binding::Variable(var_name) => {
|
||||||
|
let var_name = String::from(var_name as &str);
|
||||||
|
variables.insert(var_name.clone(), value);
|
||||||
|
self.variables_to_unbind.push(var_name);
|
||||||
|
}
|
||||||
|
Binding::Tuple(field_bindings) => {
|
||||||
|
match value {
|
||||||
|
Value::Str(_) | Value::Int(_) | Value::Bool(_) | Value::List(_) => {
|
||||||
|
// Error binding
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
Value::Reflective(reflective) => match reflective.reflect_ref() {
|
||||||
|
ReflectRef::Tuple(tuple) => {
|
||||||
|
if tuple.field_len() != field_bindings.len() {
|
||||||
|
// Error binding
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
for (field_idx, field_binding) in field_bindings.iter().enumerate() {
|
||||||
|
self.bind(
|
||||||
|
variables,
|
||||||
|
field_binding,
|
||||||
|
Value::from_reflect(
|
||||||
|
tuple.field(field_idx).unwrap().clone_value(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Error binding
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Binding::Ignore => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unbind(self, variables: &mut BTreeMap<String, Value>) {
|
||||||
|
for var_name in self.variables_to_unbind {
|
||||||
|
variables.remove(&var_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpreter<'a, O, LS> {
|
impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpreter<'a, O, LS> {
|
||||||
#[async_recursion]
|
#[async_recursion]
|
||||||
pub async fn run_steps(
|
pub async fn run_steps(
|
||||||
@ -140,7 +222,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
.map_err(|underlying| InterpreterError::OutputError { underlying })?;
|
.map_err(|underlying| InterpreterError::OutputError { underlying })?;
|
||||||
} else {
|
} else {
|
||||||
self.output
|
self.output
|
||||||
.write(&text)
|
.write(text)
|
||||||
.await
|
.await
|
||||||
.map_err(|underlying| InterpreterError::OutputError { underlying })?;
|
.map_err(|underlying| InterpreterError::OutputError { underlying })?;
|
||||||
}
|
}
|
||||||
@ -161,6 +243,20 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
.await
|
.await
|
||||||
.map_err(|underlying| InterpreterError::OutputError { underlying })?;
|
.map_err(|underlying| InterpreterError::OutputError { underlying })?;
|
||||||
}
|
}
|
||||||
|
StepDef::Set {
|
||||||
|
expression,
|
||||||
|
binding,
|
||||||
|
} => {
|
||||||
|
let expr_evaled = self.evaluate_expression(scope_idx, expression, &step.locator)?;
|
||||||
|
|
||||||
|
// Bind the expression
|
||||||
|
// Unlike the other places where we bind variables, we never unbind this one,
|
||||||
|
// since `set` is not block-scoped.
|
||||||
|
// (Ideally we would have slightly more careful scoping rules, but `set` is intentionally being
|
||||||
|
// added to allow variables to escape their respective blocks...)
|
||||||
|
let mut binder = Binder::new();
|
||||||
|
binder.bind(&mut self.scopes[scope_idx].variables, binding, expr_evaled);
|
||||||
|
}
|
||||||
StepDef::If {
|
StepDef::If {
|
||||||
condition,
|
condition,
|
||||||
true_steps,
|
true_steps,
|
||||||
@ -197,25 +293,10 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
self.run_steps(scope_idx, empty_steps).await?;
|
self.run_steps(scope_idx, empty_steps).await?;
|
||||||
} else {
|
} else {
|
||||||
for val in list {
|
for val in list {
|
||||||
// TODO duplicated code
|
let mut binder = Binder::new();
|
||||||
match binding {
|
binder.bind(&mut self.scopes[scope_idx].variables, binding, val);
|
||||||
Binding::Variable(var) => {
|
|
||||||
self.scopes[scope_idx]
|
|
||||||
.variables
|
|
||||||
.insert(String::from(var as &str), val);
|
|
||||||
}
|
|
||||||
Binding::Ignore => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.run_steps(scope_idx, body_steps).await?;
|
self.run_steps(scope_idx, body_steps).await?;
|
||||||
}
|
binder.unbind(&mut self.scopes[scope_idx].variables);
|
||||||
|
|
||||||
// TODO duplicated code
|
|
||||||
match binding {
|
|
||||||
Binding::Variable(var) => {
|
|
||||||
self.scopes[scope_idx].variables.remove(var as &str);
|
|
||||||
}
|
|
||||||
Binding::Ignore => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,25 +309,14 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
// TODO(performance) I'd like to remove this clone!
|
// TODO(performance) I'd like to remove this clone!
|
||||||
// Can possibly do so with a Yoke or something...
|
// Can possibly do so with a Yoke or something...
|
||||||
let val = list.get(idx).expect("checked iter").clone_value();
|
let val = list.get(idx).expect("checked iter").clone_value();
|
||||||
// TODO duplicated code
|
let mut binder = Binder::new();
|
||||||
match binding {
|
binder.bind(
|
||||||
Binding::Variable(var) => {
|
&mut self.scopes[scope_idx].variables,
|
||||||
self.scopes[scope_idx].variables.insert(
|
binding,
|
||||||
String::from(var as &str),
|
|
||||||
Value::from_reflect(val),
|
Value::from_reflect(val),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
Binding::Ignore => {}
|
|
||||||
}
|
|
||||||
self.run_steps(scope_idx, body_steps).await?;
|
self.run_steps(scope_idx, body_steps).await?;
|
||||||
}
|
binder.unbind(&mut self.scopes[scope_idx].variables);
|
||||||
|
|
||||||
// TODO duplicated code
|
|
||||||
match binding {
|
|
||||||
Binding::Variable(var) => {
|
|
||||||
self.scopes[scope_idx].variables.remove(var as &str);
|
|
||||||
}
|
|
||||||
Binding::Ignore => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -273,6 +343,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
let matchable_evaled =
|
let matchable_evaled =
|
||||||
self.evaluate_expression(scope_idx, matchable, &step.locator)?;
|
self.evaluate_expression(scope_idx, matchable, &step.locator)?;
|
||||||
|
|
||||||
|
let mut binder = Binder::new();
|
||||||
|
|
||||||
for (arm_binding, arm_steps) in arms {
|
for (arm_binding, arm_steps) in arms {
|
||||||
// if this arm's binding matches, then bind the variable...
|
// if this arm's binding matches, then bind the variable...
|
||||||
match arm_binding {
|
match arm_binding {
|
||||||
@ -291,10 +363,10 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
warn!(
|
// warn!(
|
||||||
"trying to `match` non-reflective vs {name} at {}",
|
// "trying to `match` non-reflective vs {name} at {}",
|
||||||
step.locator
|
// step.locator
|
||||||
);
|
// );
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -331,22 +403,12 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
for (piece, field) in
|
for (piece, field) in
|
||||||
pieces.iter().zip(reflenum.iter_fields())
|
pieces.iter().zip(reflenum.iter_fields())
|
||||||
{
|
{
|
||||||
// TODO duplicated code. Should probably make some 'Binder' tool that also makes it easy to unbind afterwards!
|
binder.bind(
|
||||||
match piece {
|
&mut self.scopes[scope_idx].variables,
|
||||||
Binding::Variable(var) => {
|
piece,
|
||||||
self.scopes[scope_idx].variables.insert(
|
Value::from_reflect(field.value().clone_value()),
|
||||||
String::from(var as &str),
|
|
||||||
// TODO would be nice to avoid this clone!
|
|
||||||
Value::from_reflect(
|
|
||||||
field.value().clone_value(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Binding::Ignore => {
|
|
||||||
// nop.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
warn!("trying to `match` weird reflective: {reflective:?} vs {name}(...) at {}", step.locator);
|
warn!("trying to `match` weird reflective: {reflective:?} vs {name}(...) at {}", step.locator);
|
||||||
@ -354,14 +416,19 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
warn!(
|
// warn!(
|
||||||
"trying to `match` non-reflective vs {name}(...) at {}",
|
// "trying to `match` non-reflective vs {name}(...) at {}",
|
||||||
step.locator
|
// step.locator
|
||||||
);
|
// );
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
MatchBinding::Variable { name } => binder.bind(
|
||||||
|
&mut self.scopes[scope_idx].variables,
|
||||||
|
&Binding::Variable(name.clone()),
|
||||||
|
matchable_evaled,
|
||||||
|
),
|
||||||
MatchBinding::Ignore => {
|
MatchBinding::Ignore => {
|
||||||
// always matches: no variable to bind, no conditions to check!
|
// always matches: no variable to bind, no conditions to check!
|
||||||
}
|
}
|
||||||
@ -371,30 +438,21 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
self.run_steps(scope_idx, arm_steps).await?;
|
self.run_steps(scope_idx, arm_steps).await?;
|
||||||
|
|
||||||
// and then unbind the variables.
|
// and then unbind the variables.
|
||||||
match arm_binding {
|
binder.unbind(&mut self.scopes[scope_idx].variables);
|
||||||
MatchBinding::TupleVariant { pieces, .. } => {
|
|
||||||
for piece in pieces {
|
|
||||||
// TODO duplicated code. Should probably make some 'Binder' tool that also makes it easy to unbind afterwards!
|
|
||||||
match piece {
|
|
||||||
Binding::Variable(var) => {
|
|
||||||
self.scopes[scope_idx].variables.remove(var as &str);
|
|
||||||
}
|
|
||||||
Binding::Ignore => {
|
|
||||||
// nop.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MatchBinding::UnitVariant { .. } | MatchBinding::Ignore => {
|
|
||||||
// no variables to unbind
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// (don't fall through to other arms since we matched this one)
|
// (don't fall through to other arms since we matched this one)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StepDef::Call { name, args, slots } => {
|
StepDef::Call { name, args, slots } => {
|
||||||
|
let Some(module) = self.program.get(name as &str) else {
|
||||||
|
return Err(InterpreterError::TypeError {
|
||||||
|
context: "Call".to_string(),
|
||||||
|
conflict: format!("no entrypoint for {name:?}."),
|
||||||
|
location: step.locator.clone(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
let mut evaled_args = BTreeMap::new();
|
let mut evaled_args = BTreeMap::new();
|
||||||
for (key, expr) in args {
|
for (key, expr) in args {
|
||||||
evaled_args.insert(
|
evaled_args.insert(
|
||||||
@ -414,23 +472,67 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.scopes.push(Scope {
|
// TODO check slots
|
||||||
variables: evaled_args,
|
// ...
|
||||||
slots: filled_in_slots,
|
|
||||||
});
|
|
||||||
let next_scope_idx = self.scopes.len() - 1;
|
|
||||||
|
|
||||||
let steps = if let Some(steps) = self.program.get(name as &str) {
|
// check params and evaluate defaults
|
||||||
steps
|
if let Some(param_defs) = &module.param_defs {
|
||||||
} else {
|
for param_def in param_defs {
|
||||||
|
if evaled_args.contains_key(param_def.name.as_str()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(default_expr) = ¶m_def.default else {
|
||||||
return Err(InterpreterError::TypeError {
|
return Err(InterpreterError::TypeError {
|
||||||
context: "Call".to_string(),
|
context: "Call".to_string(),
|
||||||
conflict: format!("no entrypoint for {name:?}."),
|
conflict: format!(
|
||||||
|
"missing required parameter {} for {name:?}.",
|
||||||
|
param_def.name
|
||||||
|
),
|
||||||
location: step.locator.clone(),
|
location: step.locator.clone(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
self.run_steps(next_scope_idx, steps).await?;
|
// Push a temporary empty scope
|
||||||
|
// TODO in the future allow evaluation with some scope if desired.
|
||||||
|
self.scopes.push(Scope {
|
||||||
|
variables: BTreeMap::new(),
|
||||||
|
slots: BTreeMap::new(),
|
||||||
|
});
|
||||||
|
evaled_args.insert(
|
||||||
|
String::from(param_def.name.as_str()),
|
||||||
|
self.evaluate_expression(
|
||||||
|
scope_idx + 1,
|
||||||
|
default_expr,
|
||||||
|
¶m_def.locator,
|
||||||
|
)?,
|
||||||
|
);
|
||||||
|
self.scopes.pop();
|
||||||
|
}
|
||||||
|
for supplied_param in evaled_args.keys() {
|
||||||
|
if !param_defs
|
||||||
|
.iter()
|
||||||
|
.any(|def| def.name.as_str() == supplied_param)
|
||||||
|
{
|
||||||
|
return Err(InterpreterError::TypeError {
|
||||||
|
context: "Call".to_string(),
|
||||||
|
conflict: format!(
|
||||||
|
"provided non-existent parameter {} for {name:?}.",
|
||||||
|
supplied_param
|
||||||
|
),
|
||||||
|
location: Locator::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.scopes.push(Scope {
|
||||||
|
variables: evaled_args,
|
||||||
|
slots: filled_in_slots,
|
||||||
|
});
|
||||||
|
|
||||||
|
let next_scope_idx = self.scopes.len() - 1;
|
||||||
|
|
||||||
|
self.run_steps(next_scope_idx, &module.steps).await?;
|
||||||
|
|
||||||
self.scopes.pop();
|
self.scopes.pop();
|
||||||
assert_eq!(self.scopes.len(), next_scope_idx);
|
assert_eq!(self.scopes.len(), next_scope_idx);
|
||||||
@ -443,7 +545,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
return if !optional {
|
return if !optional {
|
||||||
Err(InterpreterError::TypeError {
|
Err(InterpreterError::TypeError {
|
||||||
context: format!("Required slot '{name}' not filled"),
|
context: format!("Required slot '{name}' not filled"),
|
||||||
conflict: format!("slot was left empty."),
|
conflict: "slot was left empty.".to_string(),
|
||||||
location: step.locator.clone(),
|
location: step.locator.clone(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -468,8 +570,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
) -> Result<Value, InterpreterError<LS::Error, O::Error>> {
|
) -> Result<Value, InterpreterError<LS::Error, O::Error>> {
|
||||||
match expr {
|
match expr {
|
||||||
Expression::Add { left, right } => {
|
Expression::Add { left, right } => {
|
||||||
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
|
let lval = self.evaluate_expression(scope_idx, left, loc)?;
|
||||||
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
|
let rval = self.evaluate_expression(scope_idx, right, loc)?;
|
||||||
|
|
||||||
match (lval, rval) {
|
match (lval, rval) {
|
||||||
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint + rint)),
|
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint + rint)),
|
||||||
@ -481,8 +583,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::Sub { left, right } => {
|
Expression::Sub { left, right } => {
|
||||||
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
|
let lval = self.evaluate_expression(scope_idx, left, loc)?;
|
||||||
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
|
let rval = self.evaluate_expression(scope_idx, right, loc)?;
|
||||||
|
|
||||||
match (lval, rval) {
|
match (lval, rval) {
|
||||||
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint - rint)),
|
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint - rint)),
|
||||||
@ -494,8 +596,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::Mul { left, right } => {
|
Expression::Mul { left, right } => {
|
||||||
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
|
let lval = self.evaluate_expression(scope_idx, left, loc)?;
|
||||||
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
|
let rval = self.evaluate_expression(scope_idx, right, loc)?;
|
||||||
|
|
||||||
match (lval, rval) {
|
match (lval, rval) {
|
||||||
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint * rint)),
|
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint * rint)),
|
||||||
@ -507,8 +609,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::Div { left, right } => {
|
Expression::Div { left, right } => {
|
||||||
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
|
let lval = self.evaluate_expression(scope_idx, left, loc)?;
|
||||||
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
|
let rval = self.evaluate_expression(scope_idx, right, loc)?;
|
||||||
|
|
||||||
match (lval, rval) {
|
match (lval, rval) {
|
||||||
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint / rint)),
|
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint / rint)),
|
||||||
@ -520,7 +622,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::Negate { sub } => {
|
Expression::Negate { sub } => {
|
||||||
let sval = self.evaluate_expression(scope_idx, &sub, loc)?;
|
let sval = self.evaluate_expression(scope_idx, sub, loc)?;
|
||||||
|
|
||||||
match sval {
|
match sval {
|
||||||
Value::Int(sint) => Ok(Value::Int(-sint)),
|
Value::Int(sint) => Ok(Value::Int(-sint)),
|
||||||
@ -532,8 +634,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::BAnd { left, right } => {
|
Expression::BAnd { left, right } => {
|
||||||
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
|
let lval = self.evaluate_expression(scope_idx, left, loc)?;
|
||||||
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
|
let rval = self.evaluate_expression(scope_idx, right, loc)?;
|
||||||
|
|
||||||
match (lval, rval) {
|
match (lval, rval) {
|
||||||
(Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool && rbool)),
|
(Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool && rbool)),
|
||||||
@ -545,8 +647,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::BOr { left, right } => {
|
Expression::BOr { left, right } => {
|
||||||
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
|
let lval = self.evaluate_expression(scope_idx, left, loc)?;
|
||||||
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
|
let rval = self.evaluate_expression(scope_idx, right, loc)?;
|
||||||
|
|
||||||
match (lval, rval) {
|
match (lval, rval) {
|
||||||
(Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool || rbool)),
|
(Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool || rbool)),
|
||||||
@ -558,7 +660,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::BNot { sub } => {
|
Expression::BNot { sub } => {
|
||||||
let sval = self.evaluate_expression(scope_idx, &sub, loc)?;
|
let sval = self.evaluate_expression(scope_idx, sub, loc)?;
|
||||||
|
|
||||||
match sval {
|
match sval {
|
||||||
Value::Bool(sbool) => Ok(Value::Bool(!sbool)),
|
Value::Bool(sbool) => Ok(Value::Bool(!sbool)),
|
||||||
@ -570,8 +672,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::Equals { left, right } => {
|
Expression::Equals { left, right } => {
|
||||||
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
|
let lval = self.evaluate_expression(scope_idx, left, loc)?;
|
||||||
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
|
let rval = self.evaluate_expression(scope_idx, right, loc)?;
|
||||||
|
|
||||||
match (lval, rval) {
|
match (lval, rval) {
|
||||||
(Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool == rbool)),
|
(Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool == rbool)),
|
||||||
@ -585,7 +687,9 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
location: loc.clone(),
|
location: loc.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
(Value::Str(lstr), Value::Str(rstr)) => Ok(Value::Bool(lstr == rstr)),
|
||||||
|
// TODO List vs List. Not *that* useful but can be occasionally. But it does involve recursion.
|
||||||
(lother, rother) => Err(InterpreterError::TypeError {
|
(lother, rother) => Err(InterpreterError::TypeError {
|
||||||
context: "Equals".to_string(),
|
context: "Equals".to_string(),
|
||||||
conflict: format!("can't test {lother:?} and {rother:?} for equality!"),
|
conflict: format!("can't test {lother:?} and {rother:?} for equality!"),
|
||||||
@ -593,9 +697,37 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Expression::LessThan { left, right } => {
|
||||||
|
let lval = self.evaluate_expression(scope_idx, left, loc)?;
|
||||||
|
let rval = self.evaluate_expression(scope_idx, right, loc)?;
|
||||||
|
|
||||||
|
match (lval, rval) {
|
||||||
|
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Bool(lint < rint)),
|
||||||
|
(Value::Str(lstr), Value::Str(rstr)) => Ok(Value::Bool(lstr < rstr)),
|
||||||
|
(lother, rother) => Err(InterpreterError::TypeError {
|
||||||
|
context: "LessThan".to_string(),
|
||||||
|
conflict: format!("can't test {lother:?} < {rother:?}!"),
|
||||||
|
location: loc.clone(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expression::LessThanOrEquals { left, right } => {
|
||||||
|
let lval = self.evaluate_expression(scope_idx, left, loc)?;
|
||||||
|
let rval = self.evaluate_expression(scope_idx, right, loc)?;
|
||||||
|
|
||||||
|
match (lval, rval) {
|
||||||
|
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Bool(lint <= rint)),
|
||||||
|
(Value::Str(lstr), Value::Str(rstr)) => Ok(Value::Bool(lstr <= rstr)),
|
||||||
|
(lother, rother) => Err(InterpreterError::TypeError {
|
||||||
|
context: "LessThanOrEquals".to_string(),
|
||||||
|
conflict: format!("can't test {lother:?} <= {rother:?}!"),
|
||||||
|
location: loc.clone(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
Expression::ListAdd { left, right } => {
|
Expression::ListAdd { left, right } => {
|
||||||
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
|
let lval = self.evaluate_expression(scope_idx, left, loc)?;
|
||||||
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
|
let rval = self.evaluate_expression(scope_idx, right, loc)?;
|
||||||
|
|
||||||
match (lval, rval) {
|
match (lval, rval) {
|
||||||
(Value::List(mut llist), Value::List(rlist)) => {
|
(Value::List(mut llist), Value::List(rlist)) => {
|
||||||
@ -617,6 +749,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
Ok(Value::List(result))
|
Ok(Value::List(result))
|
||||||
}
|
}
|
||||||
Expression::IntLiteral { val } => Ok(Value::Int(*val)),
|
Expression::IntLiteral { val } => Ok(Value::Int(*val)),
|
||||||
|
Expression::BoolLiteral(val) => Ok(Value::Bool(*val)),
|
||||||
|
Expression::NoneLiteral => Ok(Value::Reflective(Box::new(None::<()>))),
|
||||||
Expression::StringExpr(sexpr) => {
|
Expression::StringExpr(sexpr) => {
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
for piece in &sexpr.pieces {
|
for piece in &sexpr.pieces {
|
||||||
@ -674,8 +808,34 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::MethodCall { .. } => {
|
Expression::MethodCall {
|
||||||
unimplemented!()
|
obj,
|
||||||
|
ident,
|
||||||
|
args,
|
||||||
|
loc,
|
||||||
|
} => {
|
||||||
|
let Some(method) = self.methods.get(ident.as_str()) else {
|
||||||
|
return Err(InterpreterError::TypeError {
|
||||||
|
context: format!("method call to {ident:?}"),
|
||||||
|
conflict: "No method by that name!".to_string(),
|
||||||
|
location: loc.clone(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let obj_value = self.evaluate_expression(scope_idx, obj, loc)?;
|
||||||
|
let mut arg_values: Vec<Value> = Vec::with_capacity(args.len());
|
||||||
|
for arg in args {
|
||||||
|
arg_values.push(self.evaluate_expression(scope_idx, arg, loc)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
match method.call(obj_value, arg_values) {
|
||||||
|
Ok(result_val) => Ok(result_val),
|
||||||
|
Err(method_err) => Err(InterpreterError::TypeError {
|
||||||
|
context: format!("method call to {ident:?}"),
|
||||||
|
conflict: format!("method error: {method_err}"),
|
||||||
|
location: loc.clone(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Expression::Variable { name, loc } => {
|
Expression::Variable { name, loc } => {
|
||||||
let locals = &self.scopes[scope_idx].variables;
|
let locals = &self.scopes[scope_idx].variables;
|
||||||
@ -696,6 +856,54 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
Expression::FunctionCall { .. } => {
|
Expression::FunctionCall { .. } => {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
Expression::Unwrap { obj, loc } => {
|
||||||
|
let obj_value = self.evaluate_expression(scope_idx, obj, loc)?;
|
||||||
|
|
||||||
|
match &obj_value {
|
||||||
|
Value::Reflective(reflective) => match reflective.reflect_ref() {
|
||||||
|
ReflectRef::Enum(reflenum) => match reflenum.variant_name() {
|
||||||
|
"Some" => {
|
||||||
|
if reflenum.field_len() != 1 {
|
||||||
|
return Err(InterpreterError::TypeError {
|
||||||
|
context: "unwrap".to_owned(),
|
||||||
|
conflict: "wrong number of fields in Some".to_owned(),
|
||||||
|
location: loc.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflenum.variant_type() != VariantType::Tuple {
|
||||||
|
return Err(InterpreterError::TypeError {
|
||||||
|
context: "unwrap".to_owned(),
|
||||||
|
conflict: "Some is not a tuple variant".to_owned(),
|
||||||
|
location: loc.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::from_reflect(
|
||||||
|
reflenum.field_at(0).unwrap().clone_value(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"None" => Err(InterpreterError::TypeError {
|
||||||
|
context: "unwrap".to_owned(),
|
||||||
|
conflict: "tried to unwrap None".to_owned(),
|
||||||
|
location: loc.clone(),
|
||||||
|
}),
|
||||||
|
_other => {
|
||||||
|
warn!("unnecessary unwrap (!) at {loc}");
|
||||||
|
Ok(obj_value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_other => {
|
||||||
|
warn!("unnecessary unwrap (!) at {loc}");
|
||||||
|
Ok(obj_value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_other => {
|
||||||
|
warn!("unnecessary unwrap (!) at {loc}");
|
||||||
|
Ok(obj_value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -733,9 +941,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
// too dodgy! We might want to allow this in the future, but for now let's not.
|
// too dodgy! We might want to allow this in the future, but for now let's not.
|
||||||
Err(InterpreterError::TypeError {
|
Err(InterpreterError::TypeError {
|
||||||
context: "String Interpolation".to_string(),
|
context: "String Interpolation".to_string(),
|
||||||
conflict: format!(
|
conflict: "Don't know how to write List[...] as a sensible string output."
|
||||||
"Don't know how to write List[...] as a sensible string output."
|
.to_string(),
|
||||||
),
|
|
||||||
location: loc.clone(),
|
location: loc.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -751,7 +958,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
) -> Result<(), InterpreterError<LS::Error, O::Error>> {
|
) -> Result<(), InterpreterError<LS::Error, O::Error>> {
|
||||||
match piece {
|
match piece {
|
||||||
StringPiece::Literal(lit) => {
|
StringPiece::Literal(lit) => {
|
||||||
output.push_str(&lit);
|
output.push_str(lit);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
StringPiece::Interpolation(expr) => {
|
StringPiece::Interpolation(expr) => {
|
||||||
@ -786,16 +993,63 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(mut self) -> Result<(), InterpreterError<LS::Error, O::Error>> {
|
pub async fn run(mut self) -> Result<(), InterpreterError<LS::Error, O::Error>> {
|
||||||
let main = if let Some(main) = self.program.get(&self.entrypoint) {
|
let Some(main) = self.program.get(&self.entrypoint) else {
|
||||||
main
|
|
||||||
} else {
|
|
||||||
return Err(InterpreterError::TypeError {
|
return Err(InterpreterError::TypeError {
|
||||||
context: format!("No entrypoint called {:?}", self.entrypoint),
|
context: format!("No entrypoint called {:?}", self.entrypoint),
|
||||||
conflict: "".to_string(),
|
conflict: "".to_string(),
|
||||||
location: Locator::empty(),
|
location: Locator::empty(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
self.run_steps(0, main).await?;
|
// TODO deduplicate with `Call` step
|
||||||
|
if let Some(param_defs) = &main.param_defs {
|
||||||
|
for param_def in param_defs {
|
||||||
|
if self.scopes[0]
|
||||||
|
.variables
|
||||||
|
.contains_key(param_def.name.as_str())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(default_expr) = ¶m_def.default else {
|
||||||
|
return Err(InterpreterError::TypeError {
|
||||||
|
context: "main entrypoint".to_string(),
|
||||||
|
conflict: format!(
|
||||||
|
"missing required parameter {} for entrypoint {:?}.",
|
||||||
|
param_def.name, self.entrypoint
|
||||||
|
),
|
||||||
|
location: Locator::empty(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Push a temporary empty scope
|
||||||
|
// TODO in the future allow evaluation with some scope if desired.
|
||||||
|
self.scopes.push(Scope {
|
||||||
|
variables: BTreeMap::new(),
|
||||||
|
slots: BTreeMap::new(),
|
||||||
|
});
|
||||||
|
let value = self.evaluate_expression(1, default_expr, ¶m_def.locator)?;
|
||||||
|
self.scopes[0]
|
||||||
|
.variables
|
||||||
|
.insert(String::from(param_def.name.as_str()), value);
|
||||||
|
self.scopes.pop();
|
||||||
|
}
|
||||||
|
for supplied_param in self.scopes[0].variables.keys() {
|
||||||
|
if !param_defs
|
||||||
|
.iter()
|
||||||
|
.any(|def| def.name.as_str() == supplied_param)
|
||||||
|
{
|
||||||
|
return Err(InterpreterError::TypeError {
|
||||||
|
context: "main entrypoint".to_string(),
|
||||||
|
conflict: format!(
|
||||||
|
"provided non-existent parameter {} for entrypoint {:?}.",
|
||||||
|
supplied_param, self.entrypoint
|
||||||
|
),
|
||||||
|
location: Locator::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.run_steps(0, &main.steps).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
62
hornbeam_interpreter/src/formbeam_integration.rs
Normal file
62
hornbeam_interpreter/src/formbeam_integration.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
use bevy_reflect::Reflect;
|
||||||
|
use formbeam::{FieldValidatorInfo, FormPartial, FormPartialInfo};
|
||||||
|
|
||||||
|
#[derive(Reflect)]
|
||||||
|
pub struct ReflectedForm<P: FormPartial> {
|
||||||
|
pub raw: P,
|
||||||
|
pub errors: P::Validation,
|
||||||
|
pub info: InfoWrapper,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: FormPartial> ReflectedForm<P> {
|
||||||
|
pub fn new(raw: P, errors: P::Validation) -> Self {
|
||||||
|
Self {
|
||||||
|
raw,
|
||||||
|
errors,
|
||||||
|
info: InfoWrapper(Some(P::INFO)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: FormPartial + Default> Default for ReflectedForm<P>
|
||||||
|
where
|
||||||
|
P::Validation: Default,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(Default::default(), Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Reflect)]
|
||||||
|
// This makes the struct opaque to the reflection engine, meaning it will
|
||||||
|
// be cloned absolutely instead of being converted to a DynamicTupleStruct.
|
||||||
|
// However it won't implement TupleStruct. But that's fine, that's actually what we want!
|
||||||
|
#[reflect_value]
|
||||||
|
pub struct InfoWrapper(#[reflect(ignore)] pub(crate) Option<&'static FormPartialInfo>);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Reflect)]
|
||||||
|
pub enum ReflectFieldValidatorInfo {
|
||||||
|
MinLength(u32),
|
||||||
|
MaxLength(u32),
|
||||||
|
MinValue(i64),
|
||||||
|
MaxValue(i64),
|
||||||
|
Required,
|
||||||
|
Email,
|
||||||
|
Regex(String),
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&FieldValidatorInfo> for ReflectFieldValidatorInfo {
|
||||||
|
fn from(value: &FieldValidatorInfo) -> Self {
|
||||||
|
match value {
|
||||||
|
FieldValidatorInfo::MinLength(m) => ReflectFieldValidatorInfo::MinLength(*m),
|
||||||
|
FieldValidatorInfo::MaxLength(m) => ReflectFieldValidatorInfo::MaxLength(*m),
|
||||||
|
FieldValidatorInfo::MinValue(m) => ReflectFieldValidatorInfo::MinValue(*m),
|
||||||
|
FieldValidatorInfo::MaxValue(m) => ReflectFieldValidatorInfo::MaxValue(*m),
|
||||||
|
FieldValidatorInfo::Required => ReflectFieldValidatorInfo::Required,
|
||||||
|
FieldValidatorInfo::Email => ReflectFieldValidatorInfo::Email,
|
||||||
|
FieldValidatorInfo::Regex(s) => ReflectFieldValidatorInfo::Regex((*s).to_owned()),
|
||||||
|
FieldValidatorInfo::Custom(s) => ReflectFieldValidatorInfo::Custom((*s).to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
hornbeam_interpreter/src/functions.rs
Normal file
25
hornbeam_interpreter/src/functions.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use crate::interface::Value;
|
||||||
|
|
||||||
|
pub(crate) mod defaults;
|
||||||
|
#[cfg(feature = "formbeam")]
|
||||||
|
pub(crate) mod formbeam_integration;
|
||||||
|
|
||||||
|
/// A method that can be accessed (called) by templates.
|
||||||
|
/// There is no dynamic dispatch for methods: the name of the method is the only thing that determines which one to call,
|
||||||
|
/// not the type of the self-parameter.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TemplateAccessibleMethod {
|
||||||
|
/// Function pointer that implements the method.
|
||||||
|
/// Arguments are:
|
||||||
|
/// - the 'self' parameter
|
||||||
|
/// - list of any parameters
|
||||||
|
///
|
||||||
|
/// TODO Extend this to expose interpreter state.
|
||||||
|
function: fn(Value, Vec<Value>) -> Result<Value, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateAccessibleMethod {
|
||||||
|
pub(crate) fn call(&self, obj: Value, args: Vec<Value>) -> Result<Value, String> {
|
||||||
|
(self.function)(obj, args)
|
||||||
|
}
|
||||||
|
}
|
217
hornbeam_interpreter/src/functions/defaults.rs
Normal file
217
hornbeam_interpreter/src/functions/defaults.rs
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
use std::{collections::BTreeMap, sync::Arc};
|
||||||
|
|
||||||
|
use bevy_reflect::{ReflectRef, VariantType};
|
||||||
|
use percent_encoding::NON_ALPHANUMERIC;
|
||||||
|
|
||||||
|
use crate::interface::Value;
|
||||||
|
|
||||||
|
use super::TemplateAccessibleMethod;
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
const DEFAULT_TEMPLATE_ACCESSIBLE_METHODS: &[(
|
||||||
|
&str,
|
||||||
|
fn(Value, Vec<Value>) -> Result<Value, String>,
|
||||||
|
)] = &[
|
||||||
|
("leftpad", leftpad),
|
||||||
|
("urlencode", urlencode),
|
||||||
|
("len", len),
|
||||||
|
("split", split),
|
||||||
|
("unwrap_or", unwrap_or),
|
||||||
|
("__get", __get),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Return a map of the default suggested template-accessible methods.
|
||||||
|
pub fn default_template_accessible_methods() -> BTreeMap<String, TemplateAccessibleMethod> {
|
||||||
|
DEFAULT_TEMPLATE_ACCESSIBLE_METHODS
|
||||||
|
.iter()
|
||||||
|
.map(|(name, func)| {
|
||||||
|
(
|
||||||
|
(*name).to_owned(),
|
||||||
|
TemplateAccessibleMethod { function: *func },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Left-pads a string to a given length using a given padding character.
|
||||||
|
///
|
||||||
|
/// `<Str>.leftpad(<Int>, <Str>) -> Str`
|
||||||
|
pub fn leftpad(obj: Value, args: Vec<Value>) -> Result<Value, String> {
|
||||||
|
let Value::Str(string_to_pad) = obj else {
|
||||||
|
return Err(format!("{obj:?} is not a string: can't leftpad!"));
|
||||||
|
};
|
||||||
|
if args.len() != 2 {
|
||||||
|
return Err(format!(
|
||||||
|
"leftpad takes 2 args (length, padding character), not {}",
|
||||||
|
args.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let Value::Int(pad_length) = &args[0] else {
|
||||||
|
return Err("leftpad's first arg should be an integer".to_owned());
|
||||||
|
};
|
||||||
|
let Value::Str(padding_character) = &args[1] else {
|
||||||
|
return Err(
|
||||||
|
"leftpad's second arg should be a string (usually a single character)".to_owned(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if string_to_pad.len() as i64 >= *pad_length {
|
||||||
|
// zero-clone shortcut
|
||||||
|
// also required to prevent underflow!
|
||||||
|
return Ok(Value::Str(string_to_pad));
|
||||||
|
}
|
||||||
|
|
||||||
|
let repetitions = pad_length - string_to_pad.len() as i64;
|
||||||
|
|
||||||
|
let mut result = String::new();
|
||||||
|
for _ in 0..repetitions {
|
||||||
|
result.push_str(padding_character);
|
||||||
|
}
|
||||||
|
result.push_str(&string_to_pad);
|
||||||
|
|
||||||
|
Ok(Value::Str(Arc::new(result)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// URL-encodes sensitive characters in a string.
|
||||||
|
///
|
||||||
|
/// `<Str>.urlencode() -> Str`
|
||||||
|
pub fn urlencode(obj: Value, args: Vec<Value>) -> Result<Value, String> {
|
||||||
|
let Value::Str(string_to_encode) = obj else {
|
||||||
|
return Err(format!("{obj:?} is not a string: can't urlencode!"));
|
||||||
|
};
|
||||||
|
if !args.is_empty() {
|
||||||
|
return Err(format!("urlencode takes 0 args, not {}", args.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Str(Arc::new(
|
||||||
|
percent_encoding::utf8_percent_encode(&string_to_encode, NON_ALPHANUMERIC).to_string(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the length of a given string or list.
|
||||||
|
///
|
||||||
|
/// - `<Str>.len() -> Int`
|
||||||
|
/// - `<List>.len() -> Int`
|
||||||
|
pub fn len(obj: Value, args: Vec<Value>) -> Result<Value, String> {
|
||||||
|
if !args.is_empty() {
|
||||||
|
return Err(format!("len takes 0 args, not {}", args.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Int(match obj {
|
||||||
|
Value::Str(string) => string.len() as i64,
|
||||||
|
Value::List(list) => list.len() as i64,
|
||||||
|
Value::Reflective(reflect) => match reflect.reflect_ref() {
|
||||||
|
ReflectRef::List(list) => list.len() as i64,
|
||||||
|
ReflectRef::Array(array) => array.len() as i64,
|
||||||
|
ReflectRef::Map(map) => map.len() as i64,
|
||||||
|
_ => {
|
||||||
|
return Err(format!(
|
||||||
|
"reflective {reflect:?} is not a list or map: can't get length!"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return Err(format!(
|
||||||
|
"{obj:?} is not a string or list: can't get length!"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Splits a string by given delimiters.
|
||||||
|
///
|
||||||
|
/// `<Str>.split(<Str>) -> List of Str`
|
||||||
|
pub fn split(obj: Value, args: Vec<Value>) -> Result<Value, String> {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return Err(format!("split takes 1 arg, not {}", args.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Value::Str(string_to_split) = obj else {
|
||||||
|
return Err(format!("{obj:?} is not a string: can't split!"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Value::Str(delimiter) = &args[0] else {
|
||||||
|
return Err("first arg is not a string: can't split!".to_owned());
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = string_to_split
|
||||||
|
.split(delimiter.as_str())
|
||||||
|
.map(|segment| Value::Str(Arc::new(segment.to_owned())))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Value::List(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unwraps an Option or returns the given default.
|
||||||
|
///
|
||||||
|
/// `<Option<T>>.unwrap_or(<T>) -> <T>`
|
||||||
|
pub fn unwrap_or(obj: Value, mut args: Vec<Value>) -> Result<Value, String> {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return Err(format!("unwrap_or takes 1 arg, not {}", args.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
match obj {
|
||||||
|
Value::Reflective(reflect) => match reflect.reflect_ref() {
|
||||||
|
ReflectRef::Enum(reflenum) => match reflenum.variant_name() {
|
||||||
|
"Some" => {
|
||||||
|
if reflenum.field_len() != 1 {
|
||||||
|
return Err("wrong number of fields in Some".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflenum.variant_type() != VariantType::Tuple {
|
||||||
|
return Err("Some is not a tuple variant".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::from_reflect(
|
||||||
|
reflenum.field_at(0).unwrap().clone_value(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"None" => Ok(args.pop().unwrap()),
|
||||||
|
other => Err(format!("`{other}` is not Some or None")),
|
||||||
|
},
|
||||||
|
_ => Err(format!("reflective {reflect:?} is not an Option")),
|
||||||
|
},
|
||||||
|
other => Err(format!("{other:?} is not an Option")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a field on an object by name.
|
||||||
|
///
|
||||||
|
/// `<Reflective>.__get(<Str>) -> <T>`
|
||||||
|
pub fn __get(obj: Value, args: Vec<Value>) -> Result<Value, String> {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return Err(format!("__get takes 1 arg, not {}", args.len()));
|
||||||
|
}
|
||||||
|
let Value::Str(ident) = &args[0] else {
|
||||||
|
// TODO support ints for tuple structs in the future?
|
||||||
|
return Err("first arg is not a string: can't __get!".to_owned());
|
||||||
|
};
|
||||||
|
|
||||||
|
match obj {
|
||||||
|
Value::Reflective(reflective) => {
|
||||||
|
match reflective.reflect_ref() {
|
||||||
|
ReflectRef::Struct(ss) => {
|
||||||
|
if let Some(field) = ss.field(ident) {
|
||||||
|
Ok(Value::from_reflect(field.clone_value()))
|
||||||
|
} else {
|
||||||
|
Err(format!("__get Field Lookup for '{ident}': {reflective:?} is a reflective struct that does not have that field!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReflectRef::TupleStruct(ts) => {
|
||||||
|
let field_id: Result<usize, _> = ident.parse();
|
||||||
|
if let Ok(field_id) = field_id {
|
||||||
|
if let Some(field) = ts.field(field_id) {
|
||||||
|
Ok(Value::from_reflect(field.clone_value()))
|
||||||
|
} else {
|
||||||
|
Err(format!("__get Field Lookup for '{ident}': {reflective:?} is a reflective tuple struct that does not have that field!"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(format!("__get Field Lookup for '{ident}': {reflective:?} is a reflective tuple struct that does not have names for field!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(format!("__get Field Lookup for '{ident}': {reflective:?} is of a reflective type that has no fields!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => Err(format!("{other:?} is not Reflective")),
|
||||||
|
}
|
||||||
|
}
|
110
hornbeam_interpreter/src/functions/formbeam_integration.rs
Normal file
110
hornbeam_interpreter/src/functions/formbeam_integration.rs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
use std::{collections::BTreeMap, sync::Arc};
|
||||||
|
|
||||||
|
use formbeam::FieldError;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
formbeam_integration::{InfoWrapper, ReflectFieldValidatorInfo},
|
||||||
|
interface::Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::TemplateAccessibleMethod;
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
const FORMBEAM_TEMPLATE_ACCESSIBLE_METHODS: &[(
|
||||||
|
&str,
|
||||||
|
fn(Value, Vec<Value>) -> Result<Value, String>,
|
||||||
|
)] = &[
|
||||||
|
("field_validators", field_validators),
|
||||||
|
("error_code", error_code),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Return a map of the default suggested template-accessible methods.
|
||||||
|
pub fn formbeam_template_accessible_methods() -> BTreeMap<String, TemplateAccessibleMethod> {
|
||||||
|
FORMBEAM_TEMPLATE_ACCESSIBLE_METHODS
|
||||||
|
.iter()
|
||||||
|
.map(|(name, func)| {
|
||||||
|
(
|
||||||
|
(*name).to_owned(),
|
||||||
|
TemplateAccessibleMethod { function: *func },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the validators for the given-named field.
|
||||||
|
///
|
||||||
|
/// - `<InfoWrapper>.field_validators(<Str>) -> ...`
|
||||||
|
pub fn field_validators(obj: Value, args: Vec<Value>) -> Result<Value, String> {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return Err(format!("field_validators takes 1 arg, not {}", args.len()));
|
||||||
|
}
|
||||||
|
let info_wrapper = match obj {
|
||||||
|
Value::Reflective(reflect) => match reflect.downcast::<InfoWrapper>() {
|
||||||
|
Ok(info_wrapper) => info_wrapper,
|
||||||
|
Err(not_an_info_wrapper) => {
|
||||||
|
return Err(format!(
|
||||||
|
"{not_an_info_wrapper:?} is reflective but not an InfoWrapper!"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return Err(format!("{obj:?} is not an InfoWrapper!"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let field_name = match args.first().unwrap() {
|
||||||
|
Value::Str(s) => s,
|
||||||
|
other => {
|
||||||
|
return Err(format!(
|
||||||
|
"{other:?} is not a string so cannot be a field name!"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let info = (*info_wrapper).0.as_ref().unwrap();
|
||||||
|
|
||||||
|
for field in info.fields {
|
||||||
|
if field.name == field_name.as_str() {
|
||||||
|
return Ok(Value::List(
|
||||||
|
field
|
||||||
|
.validators
|
||||||
|
.iter()
|
||||||
|
.map(|validator| {
|
||||||
|
Value::from_reflect(Box::new(ReflectFieldValidatorInfo::from(validator)))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!(
|
||||||
|
"No such field by the name of {field_name} on this form!"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the 'code' of an error, suitable for passing to the localisation engine as a key.
|
||||||
|
///
|
||||||
|
/// - `<FieldError>.error_code() -> Str`
|
||||||
|
pub fn error_code(obj: Value, args: Vec<Value>) -> Result<Value, String> {
|
||||||
|
if !args.is_empty() {
|
||||||
|
return Err(format!("error_code takes 0 args, not {}", args.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let field_error = match obj {
|
||||||
|
Value::Reflective(reflect) => match reflect.downcast::<FieldError>() {
|
||||||
|
Ok(ferr) => ferr,
|
||||||
|
Err(not_an_info_wrapper) => {
|
||||||
|
return Err(format!(
|
||||||
|
"{not_an_info_wrapper:?} is reflective but not a FieldError!"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return Err(format!("{obj:?} is not a FieldError!"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Value::Str(Arc::new(field_error.error_code().to_owned())))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO error_args
|
@ -1,9 +1,10 @@
|
|||||||
use crate::engine::{Interpreter, Scope};
|
use crate::engine::{Interpreter, Scope};
|
||||||
|
use crate::functions::TemplateAccessibleMethod;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use bevy_reflect::Reflect;
|
use bevy_reflect::Reflect;
|
||||||
use hornbeam_grammar::parse_template;
|
use hornbeam_grammar::parse_template;
|
||||||
use hornbeam_ir::ast_to_optimised_ir;
|
use hornbeam_ir::ast_to_optimised_ir;
|
||||||
use hornbeam_ir::ir::Step;
|
use hornbeam_ir::ir::TemplateFunction;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
@ -38,12 +39,16 @@ pub trait OutputSystem {
|
|||||||
|
|
||||||
// Value is currently used in the localisation system. We might pull it away later on...
|
// Value is currently used in the localisation system. We might pull it away later on...
|
||||||
pub use crate::engine::Value;
|
pub use crate::engine::Value;
|
||||||
use crate::InterpreterError;
|
#[cfg(feature = "formbeam")]
|
||||||
|
use crate::formbeam_template_accessible_methods;
|
||||||
|
use crate::{default_template_accessible_methods, InterpreterError};
|
||||||
|
|
||||||
pub struct LoadedTemplates<LS> {
|
pub struct LoadedTemplates<LS> {
|
||||||
// todo might be tempted to use e.g. ouroboros here, to keep the file source adjacent?
|
// todo might be tempted to use e.g. ouroboros here, to keep the file source adjacent?
|
||||||
// or do we just staticify?
|
// or do we just staticify?
|
||||||
template_functions: BTreeMap<String, Arc<Vec<Step>>>,
|
template_functions: BTreeMap<String, Arc<TemplateFunction>>,
|
||||||
|
|
||||||
|
methods: Arc<BTreeMap<String, TemplateAccessibleMethod>>,
|
||||||
|
|
||||||
localisation: Arc<LS>,
|
localisation: Arc<LS>,
|
||||||
}
|
}
|
||||||
@ -63,12 +68,26 @@ impl Params {
|
|||||||
|
|
||||||
impl<'a, LS> LoadedTemplates<LS> {
|
impl<'a, LS> LoadedTemplates<LS> {
|
||||||
pub fn new(localisation_system: LS) -> Self {
|
pub fn new(localisation_system: LS) -> Self {
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut methods = default_template_accessible_methods();
|
||||||
|
#[cfg(feature = "formbeam")]
|
||||||
|
methods.extend(formbeam_template_accessible_methods());
|
||||||
LoadedTemplates {
|
LoadedTemplates {
|
||||||
template_functions: Default::default(),
|
template_functions: Default::default(),
|
||||||
|
methods: Arc::new(methods),
|
||||||
localisation: Arc::new(localisation_system),
|
localisation: Arc::new(localisation_system),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_methods(
|
||||||
|
&mut self,
|
||||||
|
methods: impl IntoIterator<Item = (String, TemplateAccessibleMethod)>,
|
||||||
|
) {
|
||||||
|
let registered_methods = Arc::make_mut(&mut self.methods);
|
||||||
|
|
||||||
|
registered_methods.extend(methods);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn unload_template(&mut self, template_name: &str) -> bool {
|
pub fn unload_template(&mut self, template_name: &str) -> bool {
|
||||||
let was_removed = self.template_functions.remove(template_name).is_some();
|
let was_removed = self.template_functions.remove(template_name).is_some();
|
||||||
|
|
||||||
@ -151,8 +170,10 @@ impl<'a, LS> LoadedTemplates<LS> {
|
|||||||
params: Params,
|
params: Params,
|
||||||
locale: String,
|
locale: String,
|
||||||
) -> PreparedTemplate<LS> {
|
) -> PreparedTemplate<LS> {
|
||||||
|
// TODO add support for running an `init` or `pre` fragment before running any fragment
|
||||||
|
// This would allow boilerplate `set` statements to be done ahead of time for example...
|
||||||
PreparedTemplate {
|
PreparedTemplate {
|
||||||
all_instructions: Arc::new(self.template_functions.clone()),
|
template_functions: Arc::new(self.template_functions.clone()),
|
||||||
entrypoint: if let Some(frag) = fragment_name {
|
entrypoint: if let Some(frag) = fragment_name {
|
||||||
format!("{template_name}__{frag}")
|
format!("{template_name}__{frag}")
|
||||||
} else {
|
} else {
|
||||||
@ -161,12 +182,14 @@ impl<'a, LS> LoadedTemplates<LS> {
|
|||||||
variables: params,
|
variables: params,
|
||||||
localisation: self.localisation.clone(),
|
localisation: self.localisation.clone(),
|
||||||
locale,
|
locale,
|
||||||
|
methods: self.methods.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PreparedTemplate<LS> {
|
pub struct PreparedTemplate<LS> {
|
||||||
pub(crate) all_instructions: Arc<BTreeMap<String, Arc<Vec<Step>>>>,
|
pub(crate) template_functions: Arc<BTreeMap<String, Arc<TemplateFunction>>>,
|
||||||
|
pub(crate) methods: Arc<BTreeMap<String, TemplateAccessibleMethod>>,
|
||||||
pub(crate) entrypoint: String,
|
pub(crate) entrypoint: String,
|
||||||
pub(crate) variables: Params,
|
pub(crate) variables: Params,
|
||||||
pub(crate) localisation: Arc<LS>,
|
pub(crate) localisation: Arc<LS>,
|
||||||
@ -180,7 +203,7 @@ impl<LS: LocalisationSystem + Sync + Send> PreparedTemplate<LS> {
|
|||||||
) -> Result<(), InterpreterError<LS::Error, O::Error>> {
|
) -> Result<(), InterpreterError<LS::Error, O::Error>> {
|
||||||
let interpreter = Interpreter {
|
let interpreter = Interpreter {
|
||||||
entrypoint: self.entrypoint,
|
entrypoint: self.entrypoint,
|
||||||
program: &self.all_instructions,
|
program: &self.template_functions,
|
||||||
output,
|
output,
|
||||||
localisation: self.localisation,
|
localisation: self.localisation,
|
||||||
locale: self.locale,
|
locale: self.locale,
|
||||||
@ -188,6 +211,7 @@ impl<LS: LocalisationSystem + Sync + Send> PreparedTemplate<LS> {
|
|||||||
variables: self.variables.params,
|
variables: self.variables.params,
|
||||||
slots: Default::default(),
|
slots: Default::default(),
|
||||||
}],
|
}],
|
||||||
|
methods: &self.methods,
|
||||||
};
|
};
|
||||||
interpreter.run().await?;
|
interpreter.run().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
mod engine;
|
mod engine;
|
||||||
|
#[cfg(feature = "formbeam")]
|
||||||
|
pub mod formbeam_integration;
|
||||||
|
mod functions;
|
||||||
pub(crate) mod interface;
|
pub(crate) mod interface;
|
||||||
|
|
||||||
pub mod localisation;
|
pub mod localisation;
|
||||||
@ -26,14 +29,18 @@ pub enum InterpreterError<LE: Debug + Clone, OE: Debug> {
|
|||||||
#[error("failed to write to output: {underlying:?}")]
|
#[error("failed to write to output: {underlying:?}")]
|
||||||
OutputError { underlying: OE },
|
OutputError { underlying: OE },
|
||||||
|
|
||||||
#[error("failed to parse template: ")]
|
#[error("failed to parse template: {0}")]
|
||||||
ParseError(#[from] ParseError),
|
ParseError(#[from] ParseError),
|
||||||
|
|
||||||
#[error("failed to process parsed template: ")]
|
#[error("failed to process parsed template: {0}")]
|
||||||
AstToIrError(#[from] AstToIrError),
|
AstToIrError(#[from] AstToIrError),
|
||||||
|
|
||||||
#[error("error finding templates to load: {0}")]
|
#[error("error finding templates to load: {0}")]
|
||||||
TemplateFindError(String),
|
TemplateFindError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub use functions::defaults::default_template_accessible_methods;
|
||||||
|
#[cfg(feature = "formbeam")]
|
||||||
|
pub use functions::formbeam_integration::formbeam_template_accessible_methods;
|
||||||
|
pub use functions::TemplateAccessibleMethod;
|
||||||
pub use interface::{LoadedTemplates, LocalisationSystem, OutputSystem, Params, PreparedTemplate};
|
pub use interface::{LoadedTemplates, LocalisationSystem, OutputSystem, Params, PreparedTemplate};
|
||||||
|
@ -24,7 +24,7 @@ impl LocalisationSystem for NoLocalisation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Localisation system that dumps debug output.
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub struct DebugLocalisationSystem;
|
pub struct DebugLocalisationSystem;
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ use thiserror::Error;
|
|||||||
|
|
||||||
fn interpreter_value_to_fluent_value(v: &Value) -> Option<FluentValue> {
|
fn interpreter_value_to_fluent_value(v: &Value) -> Option<FluentValue> {
|
||||||
match v {
|
match v {
|
||||||
Value::Str(str) => Some(FluentValue::String(Cow::Borrowed(&str))),
|
Value::Str(str) => Some(FluentValue::String(Cow::Borrowed(str))),
|
||||||
Value::Int(int) => {
|
Value::Int(int) => {
|
||||||
// This is an unstyled number
|
// This is an unstyled number
|
||||||
// TODO Support fancier numbers
|
// TODO Support fancier numbers
|
||||||
@ -97,12 +97,10 @@ impl LocalisationSystem for FluentLocalisationSystem {
|
|||||||
}
|
}
|
||||||
match self.fluent.lookup_with_args(&li, trans_key, &mapped_params) {
|
match self.fluent.lookup_with_args(&li, trans_key, &mapped_params) {
|
||||||
Some(val) => Ok(Cow::Owned(val)),
|
Some(val) => Ok(Cow::Owned(val)),
|
||||||
None => {
|
None => Err(FluentLocalisationError::NoTranslation {
|
||||||
return Err(FluentLocalisationError::NoTranslation {
|
|
||||||
trans_key: String::from(trans_key),
|
trans_key: String::from(trans_key),
|
||||||
lang_id: li.to_string(),
|
lang_id: li.to_string(),
|
||||||
});
|
}),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
130
hornbeam_interpreter/tests/snapshots.rs
Normal file
130
hornbeam_interpreter/tests/snapshots.rs
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
use bevy_reflect::Reflect;
|
||||||
|
use hornbeam_interpreter::{localisation::DebugLocalisationSystem, LoadedTemplates, Params};
|
||||||
|
use insta::assert_snapshot;
|
||||||
|
|
||||||
|
#[derive(Reflect)]
|
||||||
|
struct SimpleTestStruct {
|
||||||
|
wombat: u64,
|
||||||
|
apple: u64,
|
||||||
|
banana: String,
|
||||||
|
carrot: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn simple_test_struct() -> SimpleTestStruct {
|
||||||
|
SimpleTestStruct {
|
||||||
|
wombat: 42,
|
||||||
|
apple: 78,
|
||||||
|
banana: "banana!!!".to_owned(),
|
||||||
|
carrot: "mmm CARROT!".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn simple_render(template: &str) -> String {
|
||||||
|
let mut templates = LoadedTemplates::new(DebugLocalisationSystem);
|
||||||
|
templates
|
||||||
|
.load_template_from_str("main", template, "main.hnb")
|
||||||
|
.expect("failed to load template");
|
||||||
|
let params = Params::default()
|
||||||
|
.set("sts", simple_test_struct())
|
||||||
|
.set("five", 5);
|
||||||
|
let prepared = templates.prepare("main", None, params, "en".to_owned());
|
||||||
|
prepared.render_to_string().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn snapshot_001() {
|
||||||
|
assert_snapshot!(simple_render(
|
||||||
|
r#"
|
||||||
|
html
|
||||||
|
body
|
||||||
|
"this was a triumph :>"
|
||||||
|
br
|
||||||
|
raw "<u>making a note here, huge success</u>"
|
||||||
|
|
||||||
|
if $five == 5
|
||||||
|
"FIVE!!! $five"
|
||||||
|
br
|
||||||
|
|
||||||
|
if $five < 10
|
||||||
|
"five is less than ten!"
|
||||||
|
br
|
||||||
|
|
||||||
|
if $five > 5
|
||||||
|
"weird..."
|
||||||
|
"#
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn snapshot_002() {
|
||||||
|
assert_snapshot!(simple_render(
|
||||||
|
r#"
|
||||||
|
@localiseMe{x=5}
|
||||||
|
br
|
||||||
|
"$five"
|
||||||
|
"#
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn snapshot_003() {
|
||||||
|
assert_snapshot!(simple_render(
|
||||||
|
r#"
|
||||||
|
"unpadded: ${$sts.carrot}"
|
||||||
|
br
|
||||||
|
"padded to 15: ${$sts.carrot.leftpad(15, 'M')}"
|
||||||
|
br
|
||||||
|
"urlencoded: ${$sts.carrot.urlencode()}"
|
||||||
|
br
|
||||||
|
"length: ${$sts.carrot.len()}"
|
||||||
|
br
|
||||||
|
"split on A: "
|
||||||
|
for $part in $sts.carrot.split("A")
|
||||||
|
"($part)"
|
||||||
|
br
|
||||||
|
"#
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn snapshot_004() {
|
||||||
|
assert_snapshot!(simple_render(
|
||||||
|
r#"
|
||||||
|
for $part in $sts.carrot.split("A")
|
||||||
|
set $final_part = $part
|
||||||
|
|
||||||
|
"the final part was: $final_part"
|
||||||
|
"#
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn snapshot_005() {
|
||||||
|
assert_snapshot!(simple_render(
|
||||||
|
r#"
|
||||||
|
set $unused_var = None
|
||||||
|
|
||||||
|
match $unused_var
|
||||||
|
Some($nope) =>
|
||||||
|
"UNEXPECTED $nope"
|
||||||
|
None =>
|
||||||
|
"indeed, var not used"
|
||||||
|
"#
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn snapshot_006() {
|
||||||
|
assert_snapshot!(simple_render(
|
||||||
|
r#"
|
||||||
|
declare
|
||||||
|
param $default_param = "default value!"
|
||||||
|
|
||||||
|
param $five
|
||||||
|
param $sts
|
||||||
|
|
||||||
|
"$default_param"
|
||||||
|
"#
|
||||||
|
))
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
source: hornbeam_interpreter/tests/snapshots.rs
|
||||||
|
expression: "simple_render(r#\"\nhtml\n body\n \"this was a triumph :>\"\n br\n raw \"<u>making a note here, huge success</u>\"\n\n if $five == 5\n \"FIVE!!! $five\"\n br\n\n if $five < 10\n \"five is less than ten!\"\n br\n\n if $five > 5\n \"weird...\"\n \"#)"
|
||||||
|
---
|
||||||
|
<!DOCTYPE html><html><body>this was a triumph :><br><u>making a note here, huge success</u>FIVE!!! 5<br>five is less than ten!<br></body></html>
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
source: hornbeam_interpreter/tests/snapshots.rs
|
||||||
|
expression: "simple_render(r#\"\n@localiseMe{x=5}\nbr\n\"$five\"\n \"#)"
|
||||||
|
---
|
||||||
|
@localiseMe{{"x": Int(5)}}<br>5
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
source: hornbeam_interpreter/tests/snapshots.rs
|
||||||
|
expression: "simple_render(r#\"\n\"unpadded: ${$sts.carrot}\"\nbr\n\"padded to 15: ${$sts.carrot.leftpad(15, 'M')}\"\nbr\n\"urlencoded: ${$sts.carrot.urlencode()}\"\nbr\n\"length: ${$sts.carrot.len()}\"\nbr\n\"split on A: \"\nfor $part in $sts.carrot.split(\"A\")\n \"($part)\"\n br\n \"#)"
|
||||||
|
---
|
||||||
|
unpadded: mmm CARROT!<br>padded to 15: MMMMmmm CARROT!<br>urlencoded: mmm%20CARROT%21<br>length: 11<br>split on A: (mmm C)<br>(RROT!)<br>
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
source: hornbeam_interpreter/tests/snapshots.rs
|
||||||
|
expression: "simple_render(r#\"\nfor $part in $sts.carrot.split(\"A\")\n set $final_part = $part\n\n\"the final part was: $final_part\"\n \"#)"
|
||||||
|
---
|
||||||
|
the final part was: RROT!
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
source: hornbeam_interpreter/tests/snapshots.rs
|
||||||
|
expression: "simple_render(r#\"\nset $unused_var = None\n\nmatch $unused_var\n Some($nope) =>\n \"UNEXPECTED $nope\"\n None =>\n \"indeed, var not used\"\n \"#)"
|
||||||
|
---
|
||||||
|
indeed, var not used
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
source: hornbeam_interpreter/tests/snapshots.rs
|
||||||
|
expression: "simple_render(r#\"\ndeclare\n param $default_param = \"default value!\"\n\n\"$default_param\"\n \"#)"
|
||||||
|
---
|
||||||
|
default value!
|
@ -1,12 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "hornbeam_ir"
|
name = "hornbeam_ir"
|
||||||
version = "0.0.1"
|
description = "Intermediate representation for the Hornbeam template language"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
version = "0.0.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hornbeam_grammar = { version = "0.0.1", path = "../hornbeam_grammar" }
|
hornbeam_grammar = { version = "0.0.5", path = "../hornbeam_grammar" }
|
||||||
thiserror = "1.0.38"
|
thiserror = "1.0.38"
|
||||||
serde = { version = "1.0.152", features = ["derive"] }
|
serde = { version = "1.0.152", features = ["derive"] }
|
||||||
itertools = "0.10.5"
|
itertools = "0.10.5"
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use crate::ir::{Step, StepDef};
|
use crate::ir::{ParamDef, Step, StepDef, TemplateFunction};
|
||||||
use hornbeam_grammar::ast::{Block, Expression, StringExpr, StringPiece, Template};
|
use hornbeam_grammar::ast::{
|
||||||
use hornbeam_grammar::{intern, Locator};
|
Binding, Block, Expression, HtmlElement, MatchBinding, StringExpr, StringPiece, Template,
|
||||||
|
};
|
||||||
|
use hornbeam_grammar::{intern, IStr, Locator};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::btree_map::Entry;
|
use std::collections::btree_map::Entry;
|
||||||
@ -8,9 +10,8 @@ use std::collections::BTreeMap;
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
// TODO use the void tags
|
|
||||||
/// List of all void (self-closing) HTML tags.
|
/// List of all void (self-closing) HTML tags.
|
||||||
const VOID_TAGS: &'static [&'static str] = &[
|
const VOID_TAGS: &[&str] = &[
|
||||||
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "menuitem", "meta",
|
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "menuitem", "meta",
|
||||||
"param", "source", "track", "wbr",
|
"param", "source", "track", "wbr",
|
||||||
];
|
];
|
||||||
@ -32,10 +33,10 @@ pub enum AstToIrError {
|
|||||||
///
|
///
|
||||||
/// Fragments are extracted to `{template_name}__{fragment_name}`.
|
/// Fragments are extracted to `{template_name}__{fragment_name}`.
|
||||||
/// The top-level template is extracted to `{template_name}`.
|
/// The top-level template is extracted to `{template_name}`.
|
||||||
pub(crate) fn pull_out_entrypoints<'a>(
|
pub(crate) fn pull_out_entrypoints(
|
||||||
mut template: Template,
|
mut template: Template,
|
||||||
template_name: &str,
|
template_name: &str,
|
||||||
) -> Result<BTreeMap<String, Vec<Block>>, AstToIrError> {
|
) -> Result<BTreeMap<String, Template>, AstToIrError> {
|
||||||
let mut functions = BTreeMap::new();
|
let mut functions = BTreeMap::new();
|
||||||
|
|
||||||
for child in &mut template.blocks {
|
for child in &mut template.blocks {
|
||||||
@ -44,7 +45,7 @@ pub(crate) fn pull_out_entrypoints<'a>(
|
|||||||
|
|
||||||
match functions.entry(template_name.to_owned()) {
|
match functions.entry(template_name.to_owned()) {
|
||||||
Entry::Vacant(ve) => {
|
Entry::Vacant(ve) => {
|
||||||
ve.insert(template.blocks);
|
ve.insert(template);
|
||||||
}
|
}
|
||||||
Entry::Occupied(_) => {
|
Entry::Occupied(_) => {
|
||||||
return Err(AstToIrError::SemanticError {
|
return Err(AstToIrError::SemanticError {
|
||||||
@ -62,10 +63,10 @@ pub(crate) fn pull_out_entrypoints<'a>(
|
|||||||
|
|
||||||
/// Extract entrypoints (template fragments) from a block (recursively), replacing them with
|
/// Extract entrypoints (template fragments) from a block (recursively), replacing them with
|
||||||
/// calls to that entrypoint.
|
/// calls to that entrypoint.
|
||||||
fn pull_out_entrypoints_from_block<'a>(
|
fn pull_out_entrypoints_from_block(
|
||||||
block: &mut Block,
|
block: &mut Block,
|
||||||
template_name: &str,
|
template_name: &str,
|
||||||
target: &mut BTreeMap<String, Vec<Block>>,
|
target: &mut BTreeMap<String, Template>,
|
||||||
) -> Result<(), AstToIrError> {
|
) -> Result<(), AstToIrError> {
|
||||||
match block {
|
match block {
|
||||||
Block::HtmlElement(he) => {
|
Block::HtmlElement(he) => {
|
||||||
@ -121,7 +122,10 @@ fn pull_out_entrypoints_from_block<'a>(
|
|||||||
// function.
|
// function.
|
||||||
let mut blocks = Vec::new();
|
let mut blocks = Vec::new();
|
||||||
std::mem::swap(&mut blocks, &mut frag.blocks);
|
std::mem::swap(&mut blocks, &mut frag.blocks);
|
||||||
ve.insert(blocks);
|
ve.insert(Template {
|
||||||
|
blocks,
|
||||||
|
param_defs: None,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Entry::Occupied(_) => {
|
Entry::Occupied(_) => {
|
||||||
return Err(AstToIrError::SemanticError {
|
return Err(AstToIrError::SemanticError {
|
||||||
@ -132,27 +136,40 @@ fn pull_out_entrypoints_from_block<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Block::Text(_) | Block::DefineExpandSlot(_) => { /* nop */ }
|
Block::Text(_)
|
||||||
|
| Block::RawUnescapedHtml(_)
|
||||||
|
| Block::DefineExpandSlot(_)
|
||||||
|
| Block::SetStatement(_) => { /* nop */ }
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Step 2. Compile the AST to IR steps.
|
/// Step 2. Compile the AST to IR steps.
|
||||||
pub(crate) fn compile_functions<'a>(
|
pub(crate) fn compile_functions(
|
||||||
functions: &BTreeMap<String, Vec<Block>>,
|
functions: &BTreeMap<String, Template>,
|
||||||
) -> Result<BTreeMap<String, Vec<Step>>, AstToIrError> {
|
) -> Result<BTreeMap<String, TemplateFunction>, AstToIrError> {
|
||||||
let mut result = BTreeMap::new();
|
let mut result = BTreeMap::new();
|
||||||
for (func_name, func_blocks) in functions {
|
for (func_name, func_template) in functions {
|
||||||
let mut steps = Vec::new();
|
let mut steps = Vec::new();
|
||||||
for block in func_blocks {
|
for block in &func_template.blocks {
|
||||||
compile_ast_block_to_steps(block, &mut steps)?;
|
compile_ast_block_to_steps(block, &mut steps)?;
|
||||||
}
|
}
|
||||||
result.insert(func_name.clone(), steps);
|
|
||||||
|
let param_defs = func_template.param_defs.as_ref().map(|defs| {
|
||||||
|
defs.iter()
|
||||||
|
.map(|def| ParamDef {
|
||||||
|
name: def.name.clone(),
|
||||||
|
default: def.default.clone(),
|
||||||
|
locator: def.loc.clone(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
result.insert(func_name.clone(), TemplateFunction { param_defs, steps });
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile_ast_block_to_steps<'a>(
|
fn compile_ast_block_to_steps(
|
||||||
block: &Block,
|
block: &Block,
|
||||||
instructions: &mut Vec<Step>,
|
instructions: &mut Vec<Step>,
|
||||||
) -> Result<(), AstToIrError> {
|
) -> Result<(), AstToIrError> {
|
||||||
@ -178,7 +195,7 @@ fn compile_ast_block_to_steps<'a>(
|
|||||||
instructions.push(Step {
|
instructions.push(Step {
|
||||||
def: StepDef::WriteLiteral {
|
def: StepDef::WriteLiteral {
|
||||||
escape: false,
|
escape: false,
|
||||||
text: intern(format!(" id=\"")),
|
text: intern(" id=\""),
|
||||||
},
|
},
|
||||||
locator: he.loc.clone(),
|
locator: he.loc.clone(),
|
||||||
});
|
});
|
||||||
@ -201,73 +218,87 @@ fn compile_ast_block_to_steps<'a>(
|
|||||||
|
|
||||||
// This is only handling the case where we are the exclusive owner of all the class
|
// This is only handling the case where we are the exclusive owner of all the class
|
||||||
// names: see below for more...
|
// names: see below for more...
|
||||||
if !he.classes.is_empty() {
|
if !he.classes.is_empty() && !he.attributes.contains_key(&intern("class")) {
|
||||||
if !he.attributes.contains_key(&intern("class")) {
|
let classes = he.classes.iter().map(|istr| istr.as_str()).join(" ");
|
||||||
instructions.push(Step {
|
gen_steps_to_write_literal_attribute(he, "class", &classes, instructions);
|
||||||
def: StepDef::WriteLiteral {
|
|
||||||
escape: false,
|
|
||||||
text: intern(format!(" class=\"")),
|
|
||||||
},
|
|
||||||
locator: he.loc.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
instructions.push(Step {
|
|
||||||
def: StepDef::WriteLiteral {
|
|
||||||
escape: true,
|
|
||||||
text: intern(he.classes.iter().map(|istr| istr.as_str()).join(" ")),
|
|
||||||
},
|
|
||||||
locator: he.loc.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
instructions.push(Step {
|
|
||||||
def: StepDef::WriteLiteral {
|
|
||||||
escape: false,
|
|
||||||
text: intern("\""),
|
|
||||||
},
|
|
||||||
locator: he.loc.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write attributes
|
// Write attributes
|
||||||
for (attr_name, attr_expr) in &he.attributes {
|
for (attr_name, (attr_expr, attr_flags)) in &he.attributes {
|
||||||
instructions.push(Step {
|
let extra_inject = (attr_name.as_str() == "class" && !he.classes.is_empty())
|
||||||
def: StepDef::WriteLiteral {
|
.then(|| intern(he.classes.iter().map(|istr| istr.as_str()).join(" ") + " "));
|
||||||
escape: false,
|
if attr_flags.optional {
|
||||||
text: intern(format!(" {attr_name}=\"")),
|
// For optional fields:
|
||||||
},
|
// - if it matches Some($x), unwrap to $x and emit $x
|
||||||
locator: he.loc.clone(),
|
// - if it matches None, skip
|
||||||
});
|
// - if it matches neither, assume it's already unwrapped and emit directly
|
||||||
|
// A little bit ugly to say the least...
|
||||||
|
let mut some_stage = Vec::new();
|
||||||
|
let virtual_varname = intern("___attrval");
|
||||||
|
|
||||||
if attr_name.as_str() == "class" && !he.classes.is_empty() {
|
gen_steps_to_write_attribute(
|
||||||
// This handles the case where we need to merge class lists between an
|
he,
|
||||||
// attribute and the shorthand form.
|
attr_name,
|
||||||
instructions.push(Step {
|
&Expression::Variable {
|
||||||
def: StepDef::WriteLiteral {
|
name: virtual_varname.clone(),
|
||||||
escape: true,
|
loc: he.loc.clone(),
|
||||||
text: intern(
|
|
||||||
he.classes.iter().map(|istr| istr.as_str()).join(" ") + " ",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
locator: he.loc.clone(),
|
extra_inject.clone(),
|
||||||
});
|
&mut some_stage,
|
||||||
|
);
|
||||||
|
let binding = MatchBinding::TupleVariant {
|
||||||
|
name: intern("Some"),
|
||||||
|
pieces: vec![Binding::Variable(virtual_varname.clone())],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut arms = vec![(binding, some_stage.clone())];
|
||||||
|
|
||||||
|
if let Some(extra_inject) = extra_inject {
|
||||||
|
let mut none_stage = Vec::new();
|
||||||
|
gen_steps_to_write_literal_attribute(
|
||||||
|
he,
|
||||||
|
attr_name,
|
||||||
|
extra_inject.trim_end_matches(' '),
|
||||||
|
&mut none_stage,
|
||||||
|
);
|
||||||
|
arms.push((
|
||||||
|
MatchBinding::UnitVariant {
|
||||||
|
name: intern("None"),
|
||||||
|
},
|
||||||
|
none_stage,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
arms.push((
|
||||||
|
MatchBinding::UnitVariant {
|
||||||
|
name: intern("None"),
|
||||||
|
},
|
||||||
|
Vec::new(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
instructions.push(Step {
|
arms.push((
|
||||||
def: StepDef::WriteEval {
|
MatchBinding::Variable {
|
||||||
escape: true,
|
name: virtual_varname.clone(),
|
||||||
expr: attr_expr.clone(),
|
|
||||||
},
|
},
|
||||||
locator: he.loc.clone(),
|
some_stage,
|
||||||
});
|
));
|
||||||
|
|
||||||
instructions.push(Step {
|
instructions.push(Step {
|
||||||
def: StepDef::WriteLiteral {
|
def: StepDef::Match {
|
||||||
escape: false,
|
matchable: attr_expr.clone(),
|
||||||
text: intern("\""),
|
arms,
|
||||||
},
|
},
|
||||||
locator: he.loc.clone(),
|
locator: he.loc.clone(),
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
gen_steps_to_write_attribute(
|
||||||
|
he,
|
||||||
|
attr_name,
|
||||||
|
attr_expr,
|
||||||
|
extra_inject,
|
||||||
|
instructions,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the tag
|
// Close the tag
|
||||||
@ -313,6 +344,13 @@ fn compile_ast_block_to_steps<'a>(
|
|||||||
locator: ce.loc.clone(),
|
locator: ce.loc.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Block::SetStatement(ss) => instructions.push(Step {
|
||||||
|
def: StepDef::Set {
|
||||||
|
binding: ss.binding.clone(),
|
||||||
|
expression: ss.expression.clone(),
|
||||||
|
},
|
||||||
|
locator: ss.loc.clone(),
|
||||||
|
}),
|
||||||
Block::IfBlock(ifb) => {
|
Block::IfBlock(ifb) => {
|
||||||
let mut true_instrs = Vec::new();
|
let mut true_instrs = Vec::new();
|
||||||
let mut false_instrs = Vec::new();
|
let mut false_instrs = Vec::new();
|
||||||
@ -411,6 +449,41 @@ fn compile_ast_block_to_steps<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Block::RawUnescapedHtml(text) => {
|
||||||
|
for piece in &text.pieces {
|
||||||
|
match piece {
|
||||||
|
StringPiece::Literal(lit) => {
|
||||||
|
instructions.push(Step {
|
||||||
|
def: StepDef::WriteLiteral {
|
||||||
|
text: lit.clone(),
|
||||||
|
escape: false,
|
||||||
|
},
|
||||||
|
locator: Locator::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
StringPiece::Interpolation(expr) => {
|
||||||
|
instructions.push(Step {
|
||||||
|
def: StepDef::WriteEval {
|
||||||
|
expr: expr.clone(),
|
||||||
|
escape: false,
|
||||||
|
},
|
||||||
|
locator: Locator::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
piece @ StringPiece::Localise { .. } => {
|
||||||
|
instructions.push(Step {
|
||||||
|
def: StepDef::WriteEval {
|
||||||
|
expr: Expression::StringExpr(StringExpr {
|
||||||
|
pieces: vec![piece.clone()],
|
||||||
|
}),
|
||||||
|
escape: false,
|
||||||
|
},
|
||||||
|
locator: Locator::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Block::DefineExpandSlot(slot) => {
|
Block::DefineExpandSlot(slot) => {
|
||||||
instructions.push(Step {
|
instructions.push(Step {
|
||||||
def: StepDef::CallSlotWithParentScope {
|
def: StepDef::CallSlotWithParentScope {
|
||||||
@ -435,6 +508,81 @@ fn compile_ast_block_to_steps<'a>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn gen_steps_to_write_literal_attribute(
|
||||||
|
he: &HtmlElement,
|
||||||
|
attr_name: &str,
|
||||||
|
attr_value: &str,
|
||||||
|
instructions: &mut Vec<Step>,
|
||||||
|
) {
|
||||||
|
instructions.push(Step {
|
||||||
|
def: StepDef::WriteLiteral {
|
||||||
|
escape: false,
|
||||||
|
text: intern(format!(" {attr_name}=\"")),
|
||||||
|
},
|
||||||
|
locator: he.loc.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
instructions.push(Step {
|
||||||
|
def: StepDef::WriteLiteral {
|
||||||
|
escape: true,
|
||||||
|
text: intern(attr_value),
|
||||||
|
},
|
||||||
|
locator: he.loc.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
instructions.push(Step {
|
||||||
|
def: StepDef::WriteLiteral {
|
||||||
|
escape: false,
|
||||||
|
text: intern("\""),
|
||||||
|
},
|
||||||
|
locator: he.loc.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_steps_to_write_attribute(
|
||||||
|
he: &HtmlElement,
|
||||||
|
attr_name: &str,
|
||||||
|
attr_expr: &Expression,
|
||||||
|
extra_inject: Option<IStr>,
|
||||||
|
instructions: &mut Vec<Step>,
|
||||||
|
) {
|
||||||
|
instructions.push(Step {
|
||||||
|
def: StepDef::WriteLiteral {
|
||||||
|
escape: false,
|
||||||
|
text: intern(format!(" {attr_name}=\"")),
|
||||||
|
},
|
||||||
|
locator: he.loc.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(extra_inject) = extra_inject {
|
||||||
|
// This handles the case where we need to merge class lists between an
|
||||||
|
// attribute and the shorthand form.
|
||||||
|
instructions.push(Step {
|
||||||
|
def: StepDef::WriteLiteral {
|
||||||
|
escape: true,
|
||||||
|
text: extra_inject,
|
||||||
|
},
|
||||||
|
locator: he.loc.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
instructions.push(Step {
|
||||||
|
def: StepDef::WriteEval {
|
||||||
|
escape: true,
|
||||||
|
expr: attr_expr.clone(),
|
||||||
|
},
|
||||||
|
locator: he.loc.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
instructions.push(Step {
|
||||||
|
def: StepDef::WriteLiteral {
|
||||||
|
escape: false,
|
||||||
|
text: intern("\""),
|
||||||
|
},
|
||||||
|
locator: he.loc.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -500,4 +648,24 @@ div.stylish#myid {size=42, stringy="yup", arb=$ritrary}
|
|||||||
)
|
)
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_compile_params() {
|
||||||
|
let template = parse_template(
|
||||||
|
r#"
|
||||||
|
declare
|
||||||
|
param $ritary
|
||||||
|
|
||||||
|
param $three = 3
|
||||||
|
|
||||||
|
div {arb=$ritrary}
|
||||||
|
OtherComponent {param3=$three}
|
||||||
|
"#,
|
||||||
|
"inp",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_yaml_snapshot!(compile_functions(
|
||||||
|
&pull_out_entrypoints(template, "TemplateName").unwrap()
|
||||||
|
)
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,21 @@ pub struct Function {
|
|||||||
pub steps: Vec<Step>,
|
pub steps: Vec<Step>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct TemplateFunction {
|
||||||
|
/// `None` if we don't have static parameter information.
|
||||||
|
pub param_defs: Option<Vec<ParamDef>>,
|
||||||
|
pub steps: Vec<Step>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct ParamDef {
|
||||||
|
pub name: IStr,
|
||||||
|
// TODO type information
|
||||||
|
pub default: Option<Expression>,
|
||||||
|
pub locator: Locator,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||||
pub struct Step {
|
pub struct Step {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
@ -28,6 +43,10 @@ pub enum StepDef {
|
|||||||
escape: bool,
|
escape: bool,
|
||||||
expr: Expression,
|
expr: Expression,
|
||||||
},
|
},
|
||||||
|
Set {
|
||||||
|
expression: Expression,
|
||||||
|
binding: Binding,
|
||||||
|
},
|
||||||
If {
|
If {
|
||||||
condition: Expression,
|
condition: Expression,
|
||||||
true_steps: Vec<Step>,
|
true_steps: Vec<Step>,
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
//! For using the IR, see `hornbeam_interpreter` (dynamic) or `hornbeam_macros` (code gen).
|
//! For using the IR, see `hornbeam_interpreter` (dynamic) or `hornbeam_macros` (code gen).
|
||||||
|
|
||||||
use crate::ast_to_ir::pull_out_entrypoints;
|
use crate::ast_to_ir::pull_out_entrypoints;
|
||||||
use crate::ir::Step;
|
|
||||||
use crate::peephole::apply_all_peephole_passes;
|
use crate::peephole::apply_all_peephole_passes;
|
||||||
use hornbeam_grammar::ast::Template;
|
use hornbeam_grammar::ast::Template;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@ -19,14 +18,16 @@ mod peephole;
|
|||||||
|
|
||||||
pub use ast_to_ir::AstToIrError;
|
pub use ast_to_ir::AstToIrError;
|
||||||
|
|
||||||
|
use self::ir::TemplateFunction;
|
||||||
|
|
||||||
pub fn ast_to_optimised_ir(
|
pub fn ast_to_optimised_ir(
|
||||||
template_name: &str,
|
template_name: &str,
|
||||||
template: Template,
|
template: Template,
|
||||||
) -> Result<BTreeMap<String, Vec<Step>>, AstToIrError> {
|
) -> Result<BTreeMap<String, TemplateFunction>, AstToIrError> {
|
||||||
let entrypoints = pull_out_entrypoints(template, template_name)?;
|
let entrypoints = pull_out_entrypoints(template, template_name)?;
|
||||||
let mut compiled_funcs = ast_to_ir::compile_functions(&entrypoints)?;
|
let mut compiled_funcs = ast_to_ir::compile_functions(&entrypoints)?;
|
||||||
for steps in compiled_funcs.values_mut() {
|
for func in compiled_funcs.values_mut() {
|
||||||
apply_all_peephole_passes(steps);
|
apply_all_peephole_passes(&mut func.steps);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(compiled_funcs)
|
Ok(compiled_funcs)
|
||||||
|
@ -6,15 +6,15 @@ use crate::ir::{Step, StepDef};
|
|||||||
use hornbeam_grammar::ast::{Expression, StringPiece};
|
use hornbeam_grammar::ast::{Expression, StringPiece};
|
||||||
use hornbeam_grammar::intern;
|
use hornbeam_grammar::intern;
|
||||||
|
|
||||||
//// Peephole Machinery
|
/// Peephole Machinery
|
||||||
|
|
||||||
fn peephole<T, F: FnMut(&mut [T]) -> ()>(steps: &mut [T], peephole_width: usize, mut f: F) {
|
fn peephole<T, F: FnMut(&mut [T])>(steps: &mut [T], peephole_width: usize, mut f: F) {
|
||||||
for peephole_start in 0..(steps.len() - peephole_width + 1) {
|
for peephole_start in 0..(steps.len() - peephole_width + 1) {
|
||||||
f(&mut steps[peephole_start..peephole_start + peephole_width]);
|
f(&mut steps[peephole_start..peephole_start + peephole_width]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn peephole_opt<T, F: FnMut(&mut [Option<T>]) -> ()>(
|
fn peephole_opt<T, F: FnMut(&mut [Option<T>])>(
|
||||||
steps: &mut Vec<T>,
|
steps: &mut Vec<T>,
|
||||||
peephole_width: usize,
|
peephole_width: usize,
|
||||||
mut f: F,
|
mut f: F,
|
||||||
@ -54,6 +54,7 @@ fn apply_peephole_pass<F: Fn(&mut Vec<Step>)>(steps: &mut Vec<Step>, pass: &F) {
|
|||||||
match &mut step.def {
|
match &mut step.def {
|
||||||
StepDef::WriteLiteral { .. } => {}
|
StepDef::WriteLiteral { .. } => {}
|
||||||
StepDef::WriteEval { .. } => {}
|
StepDef::WriteEval { .. } => {}
|
||||||
|
StepDef::Set { .. } => {}
|
||||||
StepDef::If {
|
StepDef::If {
|
||||||
true_steps,
|
true_steps,
|
||||||
false_steps,
|
false_steps,
|
||||||
@ -85,7 +86,7 @@ fn apply_peephole_pass<F: Fn(&mut Vec<Step>)>(steps: &mut Vec<Step>, pass: &F) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//// Peephole Passes
|
///// Peephole Passes
|
||||||
|
|
||||||
/// Given a WriteEval step that just writes literals, convert it to a WriteLiteral step.
|
/// Given a WriteEval step that just writes literals, convert it to a WriteLiteral step.
|
||||||
fn pass_write_eval_literal_to_write_literal(steps: &mut Vec<Step>) {
|
fn pass_write_eval_literal_to_write_literal(steps: &mut Vec<Step>) {
|
||||||
@ -156,8 +157,7 @@ fn pass_combine_write_literals(steps: &mut Vec<Step>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//// Apply all passes in the preferred order
|
/// Apply all passes in the preferred order
|
||||||
|
|
||||||
pub fn apply_all_peephole_passes(steps: &mut Vec<Step>) {
|
pub fn apply_all_peephole_passes(steps: &mut Vec<Step>) {
|
||||||
apply_peephole_pass(steps, &pass_write_eval_literal_to_write_literal);
|
apply_peephole_pass(steps, &pass_write_eval_literal_to_write_literal);
|
||||||
apply_peephole_pass(steps, &pass_write_literal_preescape);
|
apply_peephole_pass(steps, &pass_write_literal_preescape);
|
||||||
@ -167,16 +167,21 @@ pub fn apply_all_peephole_passes(steps: &mut Vec<Step>) {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::ast_to_ir::{compile_functions, pull_out_entrypoints};
|
use crate::{
|
||||||
|
ast_to_ir::{compile_functions, pull_out_entrypoints},
|
||||||
|
ir::TemplateFunction,
|
||||||
|
};
|
||||||
use hornbeam_grammar::parse_template;
|
use hornbeam_grammar::parse_template;
|
||||||
use insta::assert_yaml_snapshot;
|
use insta::assert_yaml_snapshot;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
fn parse_ir_and_peephole(text: &str) -> BTreeMap<String, Vec<Step>> {
|
fn parse_ir_and_peephole(text: &str) -> BTreeMap<String, TemplateFunction> {
|
||||||
let template = parse_template(text, "inp").unwrap();
|
let template = parse_template(text, "inp").unwrap();
|
||||||
let entrypoints = pull_out_entrypoints(template, "TemplateName").unwrap();
|
let entrypoints = pull_out_entrypoints(template, "TemplateName").unwrap();
|
||||||
let mut compiled = compile_functions(&entrypoints).unwrap();
|
let mut compiled = compile_functions(&entrypoints).unwrap();
|
||||||
compiled.values_mut().for_each(apply_all_peephole_passes);
|
compiled
|
||||||
|
.values_mut()
|
||||||
|
.for_each(|func| apply_all_peephole_passes(&mut func.steps));
|
||||||
compiled
|
compiled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@ source: hornbeam_ir/src/ast_to_ir.rs
|
|||||||
expression: "compile_functions(&pull_out_entrypoints(template,\n \"TemplateName\").unwrap()).unwrap()"
|
expression: "compile_functions(&pull_out_entrypoints(template,\n \"TemplateName\").unwrap()).unwrap()"
|
||||||
---
|
---
|
||||||
TemplateName:
|
TemplateName:
|
||||||
|
param_defs: ~
|
||||||
|
steps:
|
||||||
- WriteLiteral:
|
- WriteLiteral:
|
||||||
escape: false
|
escape: false
|
||||||
text: "<div"
|
text: "<div"
|
||||||
@ -41,6 +43,8 @@ TemplateName:
|
|||||||
line: 2
|
line: 2
|
||||||
column: 1
|
column: 1
|
||||||
TemplateName__Footer:
|
TemplateName__Footer:
|
||||||
|
param_defs: ~
|
||||||
|
steps:
|
||||||
- WriteLiteral:
|
- WriteLiteral:
|
||||||
escape: true
|
escape: true
|
||||||
text: Or even adjacent ones
|
text: Or even adjacent ones
|
||||||
@ -49,6 +53,8 @@ TemplateName__Footer:
|
|||||||
line: 0
|
line: 0
|
||||||
column: 0
|
column: 0
|
||||||
TemplateName__Frag1:
|
TemplateName__Frag1:
|
||||||
|
param_defs: ~
|
||||||
|
steps:
|
||||||
- WriteLiteral:
|
- WriteLiteral:
|
||||||
escape: false
|
escape: false
|
||||||
text: "<span"
|
text: "<span"
|
||||||
@ -86,6 +92,8 @@ TemplateName__Frag1:
|
|||||||
line: 6
|
line: 6
|
||||||
column: 9
|
column: 9
|
||||||
TemplateName__Frag2:
|
TemplateName__Frag2:
|
||||||
|
param_defs: ~
|
||||||
|
steps:
|
||||||
- WriteLiteral:
|
- WriteLiteral:
|
||||||
escape: false
|
escape: false
|
||||||
text: "<div"
|
text: "<div"
|
||||||
@ -114,4 +122,3 @@ TemplateName__Frag2:
|
|||||||
filename: inp
|
filename: inp
|
||||||
line: 7
|
line: 7
|
||||||
column: 13
|
column: 13
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@ source: hornbeam_ir/src/ast_to_ir.rs
|
|||||||
expression: "compile_functions(&pull_out_entrypoints(template,\n \"TemplateName\").unwrap()).unwrap()"
|
expression: "compile_functions(&pull_out_entrypoints(template,\n \"TemplateName\").unwrap()).unwrap()"
|
||||||
---
|
---
|
||||||
TemplateName:
|
TemplateName:
|
||||||
|
param_defs: ~
|
||||||
|
steps:
|
||||||
- WriteLiteral:
|
- WriteLiteral:
|
||||||
escape: false
|
escape: false
|
||||||
text: "<div"
|
text: "<div"
|
||||||
@ -170,4 +172,3 @@ TemplateName:
|
|||||||
filename: inp
|
filename: inp
|
||||||
line: 2
|
line: 2
|
||||||
column: 1
|
column: 1
|
||||||
|
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
source: hornbeam_ir/src/ast_to_ir.rs
|
||||||
|
expression: "compile_functions(&pull_out_entrypoints(template,\n \"TemplateName\").unwrap()).unwrap()"
|
||||||
|
---
|
||||||
|
TemplateName:
|
||||||
|
param_defs:
|
||||||
|
- name: ritary
|
||||||
|
default: ~
|
||||||
|
locator:
|
||||||
|
filename: inp
|
||||||
|
line: 3
|
||||||
|
column: 5
|
||||||
|
- name: three
|
||||||
|
default:
|
||||||
|
IntLiteral:
|
||||||
|
val: 3
|
||||||
|
locator:
|
||||||
|
filename: inp
|
||||||
|
line: 5
|
||||||
|
column: 5
|
||||||
|
steps:
|
||||||
|
- WriteLiteral:
|
||||||
|
escape: false
|
||||||
|
text: "<div"
|
||||||
|
locator:
|
||||||
|
filename: inp
|
||||||
|
line: 7
|
||||||
|
column: 1
|
||||||
|
- WriteLiteral:
|
||||||
|
escape: false
|
||||||
|
text: " arb=\""
|
||||||
|
locator:
|
||||||
|
filename: inp
|
||||||
|
line: 7
|
||||||
|
column: 1
|
||||||
|
- WriteEval:
|
||||||
|
escape: true
|
||||||
|
expr:
|
||||||
|
Variable:
|
||||||
|
name: ritrary
|
||||||
|
loc:
|
||||||
|
filename: inp
|
||||||
|
line: 7
|
||||||
|
column: 10
|
||||||
|
locator:
|
||||||
|
filename: inp
|
||||||
|
line: 7
|
||||||
|
column: 1
|
||||||
|
- WriteLiteral:
|
||||||
|
escape: false
|
||||||
|
text: "\""
|
||||||
|
locator:
|
||||||
|
filename: inp
|
||||||
|
line: 7
|
||||||
|
column: 1
|
||||||
|
- WriteLiteral:
|
||||||
|
escape: false
|
||||||
|
text: ">"
|
||||||
|
locator:
|
||||||
|
filename: inp
|
||||||
|
line: 7
|
||||||
|
column: 1
|
||||||
|
- Call:
|
||||||
|
name: OtherComponent
|
||||||
|
args:
|
||||||
|
param3:
|
||||||
|
Variable:
|
||||||
|
name: three
|
||||||
|
loc:
|
||||||
|
filename: inp
|
||||||
|
line: 8
|
||||||
|
column: 28
|
||||||
|
slots:
|
||||||
|
main: []
|
||||||
|
locator:
|
||||||
|
filename: inp
|
||||||
|
line: 8
|
||||||
|
column: 5
|
||||||
|
- WriteLiteral:
|
||||||
|
escape: false
|
||||||
|
text: "</div>"
|
||||||
|
locator:
|
||||||
|
filename: inp
|
||||||
|
line: 7
|
||||||
|
column: 1
|
@ -3,6 +3,8 @@ source: hornbeam_ir/src/ast_to_ir.rs
|
|||||||
expression: "pull_out_entrypoints(template, \"TemplateName\").unwrap()"
|
expression: "pull_out_entrypoints(template, \"TemplateName\").unwrap()"
|
||||||
---
|
---
|
||||||
TemplateName:
|
TemplateName:
|
||||||
|
param_defs: ~
|
||||||
|
blocks:
|
||||||
- HtmlElement:
|
- HtmlElement:
|
||||||
name: div
|
name: div
|
||||||
children:
|
children:
|
||||||
@ -28,10 +30,14 @@ TemplateName:
|
|||||||
line: 2
|
line: 2
|
||||||
column: 1
|
column: 1
|
||||||
TemplateName__Footer:
|
TemplateName__Footer:
|
||||||
|
param_defs: ~
|
||||||
|
blocks:
|
||||||
- Text:
|
- Text:
|
||||||
pieces:
|
pieces:
|
||||||
- Literal: Or even adjacent ones
|
- Literal: Or even adjacent ones
|
||||||
TemplateName__Frag1:
|
TemplateName__Frag1:
|
||||||
|
param_defs: ~
|
||||||
|
blocks:
|
||||||
- HtmlElement:
|
- HtmlElement:
|
||||||
name: span
|
name: span
|
||||||
children:
|
children:
|
||||||
@ -53,6 +59,8 @@ TemplateName__Frag1:
|
|||||||
line: 6
|
line: 6
|
||||||
column: 9
|
column: 9
|
||||||
TemplateName__Frag2:
|
TemplateName__Frag2:
|
||||||
|
param_defs: ~
|
||||||
|
blocks:
|
||||||
- HtmlElement:
|
- HtmlElement:
|
||||||
name: div
|
name: div
|
||||||
children:
|
children:
|
||||||
@ -66,4 +74,3 @@ TemplateName__Frag2:
|
|||||||
filename: inp
|
filename: inp
|
||||||
line: 7
|
line: 7
|
||||||
column: 13
|
column: 13
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@ source: hornbeam_ir/src/peephole.rs
|
|||||||
expression: "parse_ir_and_peephole(r#\"\ndiv\n fragment Frag1\n span\n \"This is a fragment!!\"\n fragment Frag2\n div\n \"There's no problem having <<nested>> fragments!\"\n fragment Footer\n \"Or even adjacent ones\"\n \"#)"
|
expression: "parse_ir_and_peephole(r#\"\ndiv\n fragment Frag1\n span\n \"This is a fragment!!\"\n fragment Frag2\n div\n \"There's no problem having <<nested>> fragments!\"\n fragment Footer\n \"Or even adjacent ones\"\n \"#)"
|
||||||
---
|
---
|
||||||
TemplateName:
|
TemplateName:
|
||||||
|
param_defs: ~
|
||||||
|
steps:
|
||||||
- WriteLiteral:
|
- WriteLiteral:
|
||||||
escape: false
|
escape: false
|
||||||
text: "<div>"
|
text: "<div>"
|
||||||
@ -34,6 +36,8 @@ TemplateName:
|
|||||||
line: 2
|
line: 2
|
||||||
column: 1
|
column: 1
|
||||||
TemplateName__Footer:
|
TemplateName__Footer:
|
||||||
|
param_defs: ~
|
||||||
|
steps:
|
||||||
- WriteLiteral:
|
- WriteLiteral:
|
||||||
escape: false
|
escape: false
|
||||||
text: Or even adjacent ones
|
text: Or even adjacent ones
|
||||||
@ -42,6 +46,8 @@ TemplateName__Footer:
|
|||||||
line: 0
|
line: 0
|
||||||
column: 0
|
column: 0
|
||||||
TemplateName__Frag1:
|
TemplateName__Frag1:
|
||||||
|
param_defs: ~
|
||||||
|
steps:
|
||||||
- WriteLiteral:
|
- WriteLiteral:
|
||||||
escape: false
|
escape: false
|
||||||
text: "<span>This is a fragment!!</span>"
|
text: "<span>This is a fragment!!</span>"
|
||||||
@ -58,6 +64,8 @@ TemplateName__Frag1:
|
|||||||
line: 6
|
line: 6
|
||||||
column: 9
|
column: 9
|
||||||
TemplateName__Frag2:
|
TemplateName__Frag2:
|
||||||
|
param_defs: ~
|
||||||
|
steps:
|
||||||
- WriteLiteral:
|
- WriteLiteral:
|
||||||
escape: false
|
escape: false
|
||||||
text: "<div>There's no problem having <<nested>> fragments!</div>"
|
text: "<div>There's no problem having <<nested>> fragments!</div>"
|
||||||
@ -65,4 +73,3 @@ TemplateName__Frag2:
|
|||||||
filename: inp
|
filename: inp
|
||||||
line: 7
|
line: 7
|
||||||
column: 13
|
column: 13
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@ source: hornbeam_ir/src/peephole.rs
|
|||||||
expression: "parse_ir_and_peephole(r#\"\ndiv.stylish#myid {size=42, stringy=\"yup\", arb=$ritrary}\n \"This is a div with a few extras\"\n OtherComponent {param1=1, param2=\"two\", param3=$three}\n \"#)"
|
expression: "parse_ir_and_peephole(r#\"\ndiv.stylish#myid {size=42, stringy=\"yup\", arb=$ritrary}\n \"This is a div with a few extras\"\n OtherComponent {param1=1, param2=\"two\", param3=$three}\n \"#)"
|
||||||
---
|
---
|
||||||
TemplateName:
|
TemplateName:
|
||||||
|
param_defs: ~
|
||||||
|
steps:
|
||||||
- WriteLiteral:
|
- WriteLiteral:
|
||||||
escape: false
|
escape: false
|
||||||
text: "<div id=\"myid\" class=\"stylish\" arb=\""
|
text: "<div id=\"myid\" class=\"stylish\" arb=\""
|
||||||
@ -76,4 +78,3 @@ TemplateName:
|
|||||||
filename: inp
|
filename: inp
|
||||||
line: 2
|
line: 2
|
||||||
column: 1
|
column: 1
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "hornbeam_macros"
|
name = "hornbeam_macros"
|
||||||
version = "0.0.1"
|
description = "Macros for the Hornbeam template system"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
version = "0.0.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
@ -6,7 +6,7 @@ let
|
|||||||
|
|
||||||
rust-toolchain = pkgs.symlinkJoin {
|
rust-toolchain = pkgs.symlinkJoin {
|
||||||
name = "rust-toolchain";
|
name = "rust-toolchain";
|
||||||
paths = [pkgs.rustc pkgs.cargo pkgs.rustfmt pkgs.rustPlatform.rustcSrc pkgs.clippy];
|
paths = [pkgs.rustc pkgs.cargo pkgs.rustfmt pkgs.rustPlatform.rustcSrc pkgs.clippy pkgs.rust-analyzer];
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user