This commit is contained in:
Jon Schoning 2019-01-30 20:54:47 -06:00
commit 2e3e7097e6
108 changed files with 21946 additions and 0 deletions

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
.stack
.stack-work

3
.env Normal file
View file

@ -0,0 +1,3 @@
HUB_REPO=localhost
APPIMAGE=espial:espial
APPDATA=.

30
.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
dist*
static/tmp/
static/combined/
config/client_session_key.aes
*.hi
*.o
*.sqlite3
*.sqlite3-shm
*.sqlite3-wal
.hsenv*
cabal-dev/
.stack-work/
yesod-devel/
.cabal-sandbox
cabal.sandbox.config
.DS_Store
*.swp
*.keter
*~
\#*
\.#*
test-project.cabal
bookmarks-*.json
TAGS
purs/dist
purs/output
purs/generated-docs
tmp
.cache
tags

6
Dockerfile Normal file
View file

@ -0,0 +1,6 @@
#-*- mode:conf; -*-
FROM jonschoning/espial:scratch
WORKDIR /app
COPY . .
CMD ./espial +RTS -T

660
LICENSE Normal file
View file

@ -0,0 +1,660 @@
### 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/>.

67
Makefile Normal file
View file

@ -0,0 +1,67 @@
.PHONY: clean build
all: build
build:
@stack build
build-fast:
@stack build --fast
build-watch:
@stack build --file-watch --fast --ghc-options=-fno-code
repl:
@stack ghci --test --bench --ghci-options=-fno-code --main-is=espial:exe:espial
ghcid:
@ghcid -c "stack ghci --test --bench --ghci-options=-fno-code --main-is=espial:exe:espial"
devel:
@yesod devel
migrate-createdb:
@stack exec migration -- createdb --conn espial.sqlite3
serve:
@stack exec espial -- +RTS -T
_ESPIAL_PS_ID = $$(docker-compose ps -q espial)
_LOCAL_INSTALL_PATH = $$(stack path | grep local-install-root | awk -e '{print $$2}')
_EKG_ASSETS_PATH = $$(find .stack-work -type d | grep ekg.*assets)
docker-compose-build: build
@rm -Rf dist && mkdir -p dist
@cp $(_LOCAL_INSTALL_PATH)/bin/* dist
@cp -R static dist
@rm -Rf dist/static/tmp
@cp -R config dist
@mkdir -p dist/ekg/assets
@cp -R $(_EKG_ASSETS_PATH) dist/ekg
@docker-compose build espial
docker-compose-up:
@docker-compose up --no-deps --no-build espial
docker-compose-down:
@docker-compose down
docker-compose-up-d:
@docker-compose up --no-deps --no-build -d espial
docker-compose-pull:
@docker-compose pull espial
docker-compose-push:
@docker tag localhost/espial:espial $(HUB_REPO)/espial:espial
@docker-compose push espial
docker-espial-logs:
@docker logs -f --since `date -u +%FT%TZ` $(_ESPIAL_PS_ID)
docker-espial-shell:
@$(docker_espial) sh
_HUB_REPO = ${HUB_REPO}
ifeq ($(_HUB_REPO),)
_HUB_REPO := "localhost"
endif
docker_espial = docker-compose exec espial
clean:
@stack clean

89
README.md Normal file
View file

@ -0,0 +1,89 @@
# Espial
Espial is an open-source, web-based bookmarking server.
It allows mutiple accounts, but currently intended for self-host scenarios.
The bookmarks are stored in a sqlite3 database, for ease of deployment & maintenence.
The easist way for logged-in users to add bookmarks, is with the "bookmarklet", found on the Settings page.
## demo server
log in — username: demo password: demo
https://esp.ae8.org/u:demo
![jpg](https://i.imgur.com/2viEMQj.png)
## Server Setup (from source)
1. [Install Stack](https://haskell-lang.org/get-started)
- On POSIX systems, this is usually `curl -sSL https://get.haskellstack.org/ | sh`
2. Build executables
```
stack build
```
3. Create the database
```
stack exec migration -- createdb --conn espial.sqlite3
```
4. Create a user
```
stack exec migration -- createuser --conn espial.sqlite3 --userName myusername --userPassword myuserpassword
```
5. Import a bookmark file for a user (optional)
```
stack exec migration -- importbookmarks --conn espial.sqlite3 --userName myusername --bookmarkFile sample-bookmarks.json
```
6. Start a production server:
```
stack exec espial -- +RTS -T
```
see `config/settings.yml` for changing default run-time parameters / environment variables
default app http port: `3000`
default ekg http port: `8000`
ssl: use reverse proxy
## Development
### Backend
- Install the `yesod` command line tool: `stack install yesod-bin --install-ghc`
- Start a development server:
```
yesod devel
```
### Frontend
- See `purs/` folder
## Import Bookmark file format
see `sample-bookmarks.json`, which contains a JSON array, each line containing a `FileBookmark` object.
example:
```
[ {"href":"http://raganwald.com/2018/02/23/forde.html","description":"Forde's Tenth Rule, or, \"How I Learned to Stop Worrying and \u2764\ufe0f the State Machine\"","extended":"","time":"2018-02-26T22:57:20Z","shared":"yes","toread":"yes","tags":"raganwald"},
, {"href":"http://downloads.haskell.org/~ghc/latest/docs/html/users_guide/flags.html","description":"7.6. Flag reference \u2014 Glasgow Haskell Compiler 8.2.2 User's Guide","extended":"-fprint-expanded-synonyms","time":"2018-02-26T21:52:02Z","shared":"yes","toread":"no","tags":"ghc haskell"},
]
```

99
app/DevelMain.hs Normal file
View file

@ -0,0 +1,99 @@
-- | Running your app inside GHCi.
--
-- To start up GHCi for usage with Yesod, first make sure you are in dev mode:
--
-- > cabal configure -fdev
--
-- Note that @yesod devel@ automatically sets the dev flag.
-- Now launch the repl:
--
-- > cabal repl --ghc-options="-O0 -fobject-code"
--
-- To start your app, run:
--
-- > :l DevelMain
-- > DevelMain.update
--
-- You can also call @DevelMain.shutdown@ to stop the app
--
-- You will need to add the foreign-store package to your .cabal file.
-- It is very light-weight.
--
-- If you don't use cabal repl, you will need
-- to run the following in GHCi or to add it to
-- your .ghci file.
--
-- :set -DDEVELOPMENT
--
-- There is more information about this approach,
-- on the wiki: https://github.com/yesodweb/yesod/wiki/ghci
module DevelMain where
import Prelude
import Application (getApplicationRepl, shutdownApp)
import Control.Exception (finally)
import Control.Monad ((>=>))
import Control.Concurrent
import Data.IORef
import Foreign.Store
import Network.Wai.Handler.Warp
import GHC.Word
-- | Start or restart the server.
-- newStore is from foreign-store.
-- A Store holds onto some data across ghci reloads
update :: IO ()
update = do
mtidStore <- lookupStore tidStoreNum
case mtidStore of
-- no server running
Nothing -> do
done <- storeAction doneStore newEmptyMVar
tid <- start done
_ <- storeAction (Store tidStoreNum) (newIORef tid)
return ()
-- server is already running
Just tidStore -> restartAppInNewThread tidStore
where
doneStore :: Store (MVar ())
doneStore = Store 0
-- shut the server down with killThread and wait for the done signal
restartAppInNewThread :: Store (IORef ThreadId) -> IO ()
restartAppInNewThread tidStore = modifyStoredIORef tidStore $ \tid -> do
killThread tid
withStore doneStore takeMVar
readStore doneStore >>= start
-- | Start the server in a separate thread.
start :: MVar () -- ^ Written to when the thread is killed.
-> IO ThreadId
start done = do
(port, site, app) <- getApplicationRepl
forkIO (finally (runSettings (setPort port defaultSettings) app)
-- Note that this implies concurrency
-- between shutdownApp and the next app that is starting.
-- Normally this should be fine
(putMVar done () >> shutdownApp site))
-- | kill the server
shutdown :: IO ()
shutdown = do
mtidStore <- lookupStore tidStoreNum
case mtidStore of
-- no server running
Nothing -> putStrLn "no Yesod app running"
Just tidStore -> do
withStore tidStore $ readIORef >=> killThread
putStrLn "Yesod app is shutdown"
tidStoreNum :: Word32
tidStoreNum = 1
modifyStoredIORef :: Store (IORef a) -> (a -> IO a) -> IO ()
modifyStoredIORef store f = withStore store $ \ref -> do
v <- readIORef ref
f v >>= writeIORef ref

6
app/devel.hs Normal file
View file

@ -0,0 +1,6 @@
{-# LANGUAGE PackageImports #-}
import "espial" Application (develMain)
import Prelude (IO)
main :: IO ()
main = develMain

5
app/main.hs Normal file
View file

@ -0,0 +1,5 @@
import Prelude (IO)
import Application (appMain)
main :: IO ()
main = appMain

83
app/migration/Main.hs Normal file
View file

@ -0,0 +1,83 @@
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
module Main where
import Types
import Model
import ModelCustom
import qualified Database.Persist as P
import qualified Database.Persist.Sqlite as P
import ClassyPrelude
import Lens.Micro
import Options.Generic
data MigrationOpts
= CreateDB { conn :: Text}
| CreateUser { conn :: Text
, userName :: Text
, userPassword :: Text
, userApiToken :: Maybe Text }
| DeleteUser { conn :: Text
, userName :: Text}
| ImportBookmarks { conn :: Text
, userName :: Text
, bookmarkFile :: FilePath}
| ImportNotes { conn :: Text
, userName :: Text
, noteDirectory :: FilePath}
| PrintMigrateDB { conn :: Text}
deriving (Generic, Show)
instance ParseRecord MigrationOpts
main :: IO ()
main = do
args <- getRecord "Migrations"
case args of
PrintMigrateDB conn ->
P.runSqlite conn dumpMigration
CreateDB conn -> do
let connInfo = P.mkSqliteConnectionInfo conn
& set P.fkEnabled False
P.runSqliteInfo connInfo runMigrations
CreateUser conn uname upass utoken ->
P.runSqlite conn $ do
hash' <- liftIO (hashPassword upass)
void $ P.upsertBy
(UniqueUserName uname)
(User uname hash' utoken False False False)
[ UserPasswordHash P.=. hash'
, UserApiToken P.=. utoken
, UserPrivateDefault P.=. False
, UserArchiveDefault P.=. False
, UserPrivacyLock P.=. False
]
pure () :: DB ()
DeleteUser conn uname ->
P.runSqlite conn $ do
muser <- P.getBy (UniqueUserName uname)
case muser of
Nothing -> liftIO (print (uname ++ "not found"))
Just (P.Entity uid _) -> do
P.deleteCascade uid
pure () :: DB ()
ImportBookmarks conn uname file ->
P.runSqlite conn $ do
muser <- P.getBy (UniqueUserName uname)
case muser of
Just (P.Entity uid _) -> insertFileBookmarks uid file
Nothing -> liftIO (print (uname ++ "not found"))
ImportNotes conn uname dir ->
P.runSqlite conn $ do
muser <- P.getBy (UniqueUserName uname)
case muser of
Just (P.Entity uid _) -> insertDirFileNotes uid dir
Nothing -> liftIO (print (uname ++ "not found"))

4
changelog.md Normal file
View file

@ -0,0 +1,4 @@
__v0.0.7__
init

BIN
config/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

70
config/keter.yml Normal file
View file

@ -0,0 +1,70 @@
# After you've edited this file, remove the following line to allow
# `yesod keter` to build your bundle.
user-edited: false
# A Keter app is composed of 1 or more stanzas. The main stanza will define our
# web application. See the Keter documentation for more information on
# available stanzas.
stanzas:
# Your Yesod application.
- type: webapp
# Name of your executable. You are unlikely to need to change this.
# Note that all file paths are relative to the keter.yml file.
#
# The path given is for Stack projects. If you're still using cabal, change
# to
# exec: ../dist/build/espial/espial
exec: ../dist/bin/espial
# Command line options passed to your application.
args: []
hosts:
# You can specify one or more hostnames for your application to respond
# to. The primary hostname will be used for generating your application
# root.
- www.espial.com
# Enable to force Keter to redirect to https
# Can be added to any stanza
requires-secure: false
# Static files.
- type: static-files
hosts:
- static.espial.com
root: ../static
# Uncomment to turn on directory listings.
# directory-listing: true
# Redirect plain domain name to www.
- type: redirect
hosts:
- espial.com
actions:
- host: www.espial.com
# secure: false
# port: 80
# Uncomment to switch to a non-permanent redirect.
# status: 303
# Use the following to automatically copy your bundle upon creation via `yesod
# keter`. Uses `scp` internally, so you can set it to a remote destination
# copy-to: user@host:/opt/keter/incoming/
# You can pass arguments to `scp` used above. This example limits bandwidth to
# 1024 Kbit/s and uses port 2222 instead of the default 22
# copy-to-args:
# - "-l 1024"
# - "-P 2222"
# If you would like to have Keter automatically create a PostgreSQL database
# and set appropriate environment variables for it to be discovered, uncomment
# the following line.
# plugins:
# postgres: true

1
config/robots.txt Normal file
View file

@ -0,0 +1 @@
User-agent: *

40
config/routes Normal file
View file

@ -0,0 +1,40 @@
/static StaticR Static appStatic
/favicon.ico FaviconR GET
/robots.txt RobotsR GET
/auth AuthR Auth getAuth
-- notes
!/#UserNameP/notes NotesR GET
!/#UserNameP/notes/add AddNoteViewR GET
!/#UserNameP/notes/#NtSlug NoteR GET
!/api/note/add AddNoteR POST
!/api/note/#Int64 DeleteNoteR DELETE
-- user
/ HomeR GET
!/#UserNameP UserR GET
!/#UserNameP/#SharedP UserSharedR GET
!/#UserNameP/#FilterP UserFilterR GET
!/#UserNameP/#TagsP UserTagsR GET
-- settings
/Settings AccountSettingsR GET
api/accountSettings EditAccountSettingsR POST
-- settings/password
/Settings/Password ChangePasswordR GET POST
-- add
/add AddViewR GET
api/add AddR POST
-- edit
/bm/#Int64 DeleteR DELETE
/bm/#Int64/read ReadR POST
/bm/#Int64/star StarR POST
/bm/#Int64/unstar UnstarR POST
-- doc
/docs/search DocsSearchR GET

41
config/settings.yml Normal file
View file

@ -0,0 +1,41 @@
# Values formatted like "_env:ENV_VAR_NAME:default_value" can be overridden by the specified environment variable.
# See https://github.com/yesodweb/yesod/wiki/Configuration#overriding-configuration-values-with-environment-variables
static-dir: "_env:STATIC_DIR:static"
host: "_env:HOST:*4" # any IPv4 host
port: "_env:PORT:3000" # NB: The port `yesod devel` uses is distinct from this value. Set the `yesod devel` port from the command line.
ip-from-header: "_env:IP_FROM_HEADER:false"
# Default behavior: determine the application root from the request headers.
# Uncomment to set an explicit approot
#approot: "_env:APPROOT:http://localhost:3000"
# By default, `yesod devel` runs in development, and built executables use
# production settings (see below). To override this, use the following:
#
# development: false
# Optional values with the following production defaults.
# In development, they default to the inverse.
#
# detailed-logging: false
# should-log-all: false
# reload-templates: false
# mutable-static: false
# skip-combining: false
# auth-dummy-login : false
# NB: If you need a numeric value (e.g. 123) to parse as a String, wrap it in single quotes (e.g. "_env:PGPASS:'123'")
# See https://github.com/yesodweb/yesod/wiki/Configuration#parsing-numeric-values-as-strings
database:
# See config/test-settings.yml for an override during tests
database: "_env:SQLITE_DATABASE:espial.sqlite3"
# database: ":memory:"
poolsize: "_env:SQLITE_POOLSIZE:10"
copyright: Insert copyright statement here
#analytics: UA-YOURCODE
ekg-host: "_env:EKG_HOST:0.0.0.0"
ekg-port: "_env:EKG_PORT:8000"

12
config/test-settings.yml Normal file
View file

@ -0,0 +1,12 @@
database:
# NOTE: By design, this setting prevents the SQLITE_DATABASE environment variable
# from affecting test runs, so that we don't accidentally affect the
# production database during testing. If you're not concerned about that and
# would like to have environment variable overrides, you could instead use
# something like:
#
# database: "_env:SQLITE_DATABASE:espial_test.sqlite3"
# database: espial_test.sqlite3
database: ":memory:"
auth-dummy-login: true

16
docker-compose.yml Normal file
View file

@ -0,0 +1,16 @@
version: '3'
services:
espial:
image: '$HUB_REPO/$APPIMAGE'
build:
context: dist
dockerfile: ../Dockerfile
ports:
- "3000:3000"
- "8000:8000"
volumes:
- '$APPDATA:/app/data'
environment:
- IP_FROM_HEADER=true
- SQLITE_DATABASE=/app/data/espial.sqlite3
- ekg_datadir=ekg

406
espial.cabal Normal file
View file

@ -0,0 +1,406 @@
-- This file has been generated from package.yaml by hpack version 0.28.2.
--
-- see: https://github.com/sol/hpack
--
-- hash: 417de4bead54d60a2c091ad91c61dc715571ef7421e702f157a3766daf4f4700
name: espial
version: 0.0.7
synopsis: Espial is an open-source, web-based bookmarking server.
description: .
Espial is an open-source, web-based bookmarking server.
- Yesod + PureScript + sqlite3
- multi-user (w/ privacy scopes)
- tags, stars, editing, deleting
category: Web
homepage: https://github.com/jonschoning/espial
bug-reports: https://github.com/jonschoning/espial/issues
author: Jon Schoning
maintainer: jonschoning@gmail.com
copyright: Copyright (c) 2018 Jon Schoning
license: AGPL-3
license-file: LICENSE
build-type: Simple
cabal-version: >= 1.10
extra-source-files:
changelog.md
config/favicon.ico
config/keter.yml
config/robots.txt
config/routes
config/settings.yml
config/test-settings.yml
purs/Makefile
purs/packages.dhall
purs/spago.dhall
purs/src/App.purs
purs/src/Component/AccountSettings.purs
purs/src/Component/Add.purs
purs/src/Component/BList.purs
purs/src/Component/BMark.purs
purs/src/Component/Markdown.purs
purs/src/Component/NList.purs
purs/src/Component/NNote.purs
purs/src/Component/RawHtml.js
purs/src/Component/RawHtml.purs
purs/src/Globals.js
purs/src/Globals.purs
purs/src/Main.purs
purs/src/Marked.js
purs/src/Marked.purs
purs/src/Model.purs
purs/src/Util.purs
purs/test/Main.purs
README.md
static/css/main.css
static/css/popup.css
static/css/tachyons.min.css
static/css/tachyons.min.css.gz
static/images/bluepin.gif
static/js/app.js
static/js/app.js.gz
static/js/app.min.js
static/js/app.min.js.gz
static/js/html5shiv.min.js
static/js/html5shiv.min.js.gz
static/js/js.cookie-2.2.0.min.js
static/js/js.cookie-2.2.0.min.js.gz
static/js/marked.min.js
static/js/marked.min.js.gz
static/js/moment.min.js
static/js/moment.min.js.gz
templates/change-password.hamlet
templates/default-layout-wrapper.hamlet
templates/default-layout.hamlet
templates/docs-search.hamlet
templates/homepage.hamlet
templates/login.hamlet
templates/note.hamlet
templates/notes.hamlet
templates/pager.hamlet
templates/popup-layout.hamlet
templates/search.hamlet
templates/user-settings.hamlet
templates/user.hamlet
source-repository head
type: git
location: git://github.com/jonschoning/espial.git
flag dev
description: Turn on development settings, like auto-reload templates.
manual: False
default: False
flag library-only
description: Build for use with "yesod devel"
manual: False
default: False
library
hs-source-dirs:
src
default-extensions: BangPatterns CPP ConstraintKinds DataKinds DeriveDataTypeable DeriveGeneric EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NoImplicitPrelude OverloadedStrings PolyKinds PolymorphicComponents QuasiQuotes Rank2Types RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators TypeSynonymInstances ViewPatterns
build-depends:
aeson >=1.4
, attoparsec
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
, bcrypt >=0.0.8
, bytestring >=0.9 && <0.11
, case-insensitive
, classy-prelude >=1.4 && <1.6
, classy-prelude-conduit >=1.4 && <1.6
, classy-prelude-yesod >=1.4 && <1.6
, conduit >=1.0 && <2.0
, containers
, data-default
, directory >=1.1 && <1.4
, ekg
, ekg-core
, entropy
, esqueleto
, fast-logger >=2.2 && <2.5
, file-embed
, foreign-store
, hjsmin >=0.1 && <0.3
, hscolour
, http-api-data >=0.3.4
, http-client
, http-client-tls >=0.3 && <0.4
, http-conduit >=2.3 && <2.4
, http-types
, iso8601-time >=0.1.3
, microlens
, monad-logger >=0.3 && <0.4
, monad-metrics
, mtl
, parser-combinators
, persistent >=2.8 && <2.10
, persistent-sqlite >=2.6.2
, persistent-template >=2.5 && <2.9
, pretty-show
, safe
, shakespeare >=2.0 && <2.1
, template-haskell
, text >=0.11 && <2.0
, time
, transformers >=0.2.2
, unordered-containers
, vector
, wai
, wai-extra >=3.0 && <3.1
, wai-logger >=2.2 && <2.4
, wai-middleware-metrics
, warp >=3.0 && <3.3
, yaml >=0.8 && <0.12
, yesod >=1.6 && <1.7
, yesod-auth >=1.6 && <1.7
, yesod-core >=1.6 && <1.7
, yesod-form >=1.6 && <1.7
, yesod-static >=1.6 && <1.7
if (flag(dev)) || (flag(library-only))
ghc-options: -Wall -fwarn-tabs -O0
cpp-options: -DDEVELOPMENT
else
ghc-options: -Wall -fwarn-tabs -O2
exposed-modules:
Application
Foundation
Generic
Handler.AccountSettings
Handler.Add
Handler.Archive
Handler.Common
Handler.Docs
Handler.Edit
Handler.Home
Handler.Notes
Handler.User
Import
Import.NoFoundation
Model
ModelCustom
PathPiece
Pretty
Settings
Settings.StaticFiles
Types
other-modules:
Paths_espial
default-language: Haskell2010
executable espial
main-is: main.hs
hs-source-dirs:
app
default-extensions: BangPatterns CPP ConstraintKinds DataKinds DeriveDataTypeable DeriveGeneric EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NoImplicitPrelude OverloadedStrings PolyKinds PolymorphicComponents QuasiQuotes Rank2Types RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators TypeSynonymInstances ViewPatterns
ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends:
aeson >=1.4
, attoparsec
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
, bcrypt >=0.0.8
, bytestring >=0.9 && <0.11
, case-insensitive
, classy-prelude >=1.4 && <1.6
, classy-prelude-conduit >=1.4 && <1.6
, classy-prelude-yesod >=1.4 && <1.6
, conduit >=1.0 && <2.0
, containers
, data-default
, directory >=1.1 && <1.4
, ekg
, ekg-core
, entropy
, espial
, esqueleto
, fast-logger >=2.2 && <2.5
, file-embed
, foreign-store
, hjsmin >=0.1 && <0.3
, hscolour
, http-api-data >=0.3.4
, http-client
, http-client-tls >=0.3 && <0.4
, http-conduit >=2.3 && <2.4
, http-types
, iso8601-time >=0.1.3
, microlens
, monad-logger >=0.3 && <0.4
, monad-metrics
, mtl
, parser-combinators
, persistent >=2.8 && <2.10
, persistent-sqlite >=2.6.2
, persistent-template >=2.5 && <2.9
, pretty-show
, safe
, shakespeare >=2.0 && <2.1
, template-haskell
, text >=0.11 && <2.0
, time
, transformers >=0.2.2
, unordered-containers
, vector
, wai
, wai-extra >=3.0 && <3.1
, wai-logger >=2.2 && <2.4
, wai-middleware-metrics
, warp >=3.0 && <3.3
, yaml >=0.8 && <0.12
, yesod >=1.6 && <1.7
, yesod-auth >=1.6 && <1.7
, yesod-core >=1.6 && <1.7
, yesod-form >=1.6 && <1.7
, yesod-static >=1.6 && <1.7
if flag(library-only)
buildable: False
other-modules:
DevelMain
Paths_espial
default-language: Haskell2010
executable migration
main-is: Main.hs
hs-source-dirs:
app/migration
default-extensions: BangPatterns CPP ConstraintKinds DataKinds DeriveDataTypeable DeriveGeneric EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NoImplicitPrelude OverloadedStrings PolyKinds PolymorphicComponents QuasiQuotes Rank2Types RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators TypeSynonymInstances ViewPatterns
ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends:
aeson >=1.4
, attoparsec
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
, bcrypt >=0.0.8
, bytestring >=0.9 && <0.11
, case-insensitive
, classy-prelude >=1.4 && <1.6
, classy-prelude-conduit >=1.4 && <1.6
, classy-prelude-yesod >=1.4 && <1.6
, conduit >=1.0 && <2.0
, containers
, data-default
, directory >=1.1 && <1.4
, ekg
, ekg-core
, entropy
, espial
, esqueleto
, fast-logger >=2.2 && <2.5
, file-embed
, foreign-store
, hjsmin >=0.1 && <0.3
, hscolour
, http-api-data >=0.3.4
, http-client
, http-client-tls >=0.3 && <0.4
, http-conduit >=2.3 && <2.4
, http-types
, iso8601-time >=0.1.3
, microlens
, monad-logger >=0.3 && <0.4
, monad-metrics
, mtl
, optparse-generic >=1.2.3
, parser-combinators
, persistent >=2.8 && <2.10
, persistent-sqlite >=2.6.2
, persistent-template >=2.5 && <2.9
, pretty-show
, safe
, shakespeare >=2.0 && <2.1
, template-haskell
, text >=0.11 && <2.0
, time
, transformers >=0.2.2
, unordered-containers
, vector
, wai
, wai-extra >=3.0 && <3.1
, wai-logger >=2.2 && <2.4
, wai-middleware-metrics
, warp >=3.0 && <3.3
, yaml >=0.8 && <0.12
, yesod >=1.6 && <1.7
, yesod-auth >=1.6 && <1.7
, yesod-core >=1.6 && <1.7
, yesod-form >=1.6 && <1.7
, yesod-static >=1.6 && <1.7
if flag(library-only)
buildable: False
other-modules:
Paths_espial
default-language: Haskell2010
test-suite test
type: exitcode-stdio-1.0
main-is: Spec.hs
hs-source-dirs:
test
default-extensions: BangPatterns CPP ConstraintKinds DataKinds DeriveDataTypeable DeriveGeneric EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NoImplicitPrelude OverloadedStrings PolyKinds PolymorphicComponents QuasiQuotes Rank2Types RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators TypeSynonymInstances ViewPatterns
ghc-options: -Wall
build-depends:
aeson >=1.4
, attoparsec
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
, bcrypt >=0.0.8
, bytestring >=0.9 && <0.11
, case-insensitive
, classy-prelude >=1.4 && <1.6
, classy-prelude-conduit >=1.4 && <1.6
, classy-prelude-yesod >=1.4 && <1.6
, conduit >=1.0 && <2.0
, containers
, data-default
, directory >=1.1 && <1.4
, ekg
, ekg-core
, entropy
, espial
, esqueleto
, fast-logger >=2.2 && <2.5
, file-embed
, foreign-store
, hjsmin >=0.1 && <0.3
, hscolour
, hspec >=2.0.0
, http-api-data >=0.3.4
, http-client
, http-client-tls >=0.3 && <0.4
, http-conduit >=2.3 && <2.4
, http-types
, iso8601-time >=0.1.3
, microlens
, monad-logger >=0.3 && <0.4
, monad-metrics
, mtl
, parser-combinators
, persistent >=2.8 && <2.10
, persistent-sqlite >=2.6.2
, persistent-template >=2.5 && <2.9
, pretty-show
, safe
, shakespeare >=2.0 && <2.1
, template-haskell
, text >=0.11 && <2.0
, time
, transformers >=0.2.2
, unordered-containers
, vector
, wai
, wai-extra >=3.0 && <3.1
, wai-logger >=2.2 && <2.4
, wai-middleware-metrics
, warp >=3.0 && <3.3
, yaml >=0.8 && <0.12
, yesod >=1.6 && <1.7
, yesod-auth >=1.6 && <1.7
, yesod-core >=1.6 && <1.7
, yesod-form >=1.6 && <1.7
, yesod-static >=1.6 && <1.7
, yesod-test
other-modules:
Handler.CommonSpec
Handler.HomeSpec
TestImport
Paths_espial
default-language: Haskell2010

208
package.yaml Normal file
View file

@ -0,0 +1,208 @@
name: espial
synopsis: Espial is an open-source, web-based bookmarking server.
version: "0.0.7"
description: ! '
Espial is an open-source, web-based bookmarking server.
- Yesod + PureScript + sqlite3
- multi-user (w/ privacy scopes)
- tags, stars, editing, deleting
'
category: Web
author: Jon Schoning
maintainer: jonschoning@gmail.com
copyright: Copyright (c) 2018 Jon Schoning
license: AGPL-3
license-file: LICENSE
homepage: https://github.com/jonschoning/espial
git: git://github.com/jonschoning/espial.git
bug-reports: https://github.com/jonschoning/espial/issues
extra-source-files:
- README.md
- changelog.md
- config/favicon.ico
- config/keter.yml
- config/robots.txt
- config/routes
- config/settings.yml
- config/test-settings.yml
- templates/**
- static/css/**
- static/images/**
- static/js/**
- purs/Makefile
- purs/packages.dhall
- purs/spago.dhall
- purs/src/**
- purs/src/**/Component/**
- purs/test/**
default-extensions:
- BangPatterns
- CPP
- ConstraintKinds
- DataKinds
- DeriveDataTypeable
- DeriveGeneric
- EmptyDataDecls
- FlexibleContexts
- FlexibleInstances
- GADTs
- GeneralizedNewtypeDeriving
- InstanceSigs
- KindSignatures
- LambdaCase
- MultiParamTypeClasses
- MultiWayIf
- NoImplicitPrelude
- OverloadedStrings
- PolyKinds
- PolymorphicComponents
- QuasiQuotes
- Rank2Types
- RankNTypes
- RecordWildCards
- ScopedTypeVariables
- StandaloneDeriving
- TemplateHaskell
- TupleSections
- TypeApplications
- TypeFamilies
- TypeOperators
- TypeSynonymInstances
- ViewPatterns
dependencies:
# Due to a bug in GHC 8.0.1, we block its usage
# See: https://ghc.haskell.org/trac/ghc/ticket/12130
- base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
- yesod >=1.6 && <1.7
- yesod-core >=1.6 && <1.7
- yesod-auth >=1.6 && <1.7
- yesod-static >=1.6 && <1.7
- yesod-form >=1.6 && <1.7
- classy-prelude >=1.4 && <1.6
- classy-prelude-conduit >=1.4 && <1.6
- classy-prelude-yesod >=1.4 && <1.6
- bytestring >=0.9 && <0.11
- text >=0.11 && <2.0
- persistent >=2.8 && <2.10
# - persistent-postgresql >=2.8 && <2.9
- persistent-template >=2.5 && <2.9
- template-haskell
- shakespeare >=2.0 && <2.1
- hjsmin >=0.1 && <0.3
# - monad-control >=0.3 && <1.1
- wai-extra >=3.0 && <3.1
- yaml >=0.8 && <0.12
- http-client-tls >=0.3 && <0.4
- http-conduit >=2.3 && <2.4
- directory >=1.1 && <1.4
- warp >=3.0 && <3.3
- data-default
# - aeson >=0.6 && <1.4
- conduit >=1.0 && <2.0
- monad-logger >=0.3 && <0.4
- fast-logger >=2.2 && <2.5
- wai-logger >=2.2 && <2.4
- file-embed
- safe
- unordered-containers
- containers
- vector
- time
- case-insensitive
- wai
- foreign-store
- aeson >=1.4
- attoparsec
- bcrypt >= 0.0.8
- entropy
- ekg
- ekg-core
- esqueleto
- hscolour
- http-api-data >= 0.3.4
- http-client
- http-types
- iso8601-time >=0.1.3
- microlens
- monad-metrics
- mtl
- persistent-sqlite >=2.6.2
- pretty-show
- transformers >= 0.2.2
- wai-middleware-metrics
- parser-combinators
# The library contains all of our application code. The executable
# defined below is just a thin wrapper.
library:
source-dirs: src
when:
- condition: (flag(dev)) || (flag(library-only))
then:
ghc-options:
- -Wall
- -fwarn-tabs
- -O0
cpp-options: -DDEVELOPMENT
else:
ghc-options:
- -Wall
- -fwarn-tabs
- -O2
# Runnable executable for our application
executables:
espial:
main: main.hs
source-dirs: app
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- espial
when:
- condition: flag(library-only)
buildable: false
migration:
when:
- condition: flag(library-only)
buildable: false
main: Main.hs
source-dirs:
- app/migration
ghc-options: -threaded -rtsopts -with-rtsopts=-N
dependencies:
- espial
- optparse-generic >= 1.2.3
# Test suite
tests:
test:
main: Spec.hs
source-dirs: test
ghc-options: -Wall
dependencies:
- espial
- hspec >=2.0.0
- yesod-test
# Define flags used by "yesod devel" to make compilation faster
flags:
library-only:
description: Build for use with "yesod devel"
manual: false
default: false
dev:
description: Turn on development settings, like auto-reload templates.
manual: false
default: false

11
purs/.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
/.psa*
/.psc*
/.psc-package/
/.pulp-cache/
/.purs*
/.spago
/bower_components/
/generated-docs/
/node_modules/
/output/
/tmp/

28
purs/Makefile Normal file
View file

@ -0,0 +1,28 @@
.PHONY: clean build
all: build
install:
spago install
build:
@spago build
@spago bundle --to dist/app.js
@(cd dist && terser app.js -m -c -o app.min.js)
@rm -f dist/*.gz
@gzip -k dist/app.js
@gzip -k dist/app.min.js
@find dist -type f -printf "%kK\\t%h/%f\\n" | sort -k 2
@cp dist/app.js ../static/js/app.js
@cp dist/app.js.gz ../static/js/app.js.gz
@cp dist/app.min.js ../static/js/app.min.js
@cp dist/app.min.js.gz ../static/js/app.min.js.gz
docs:
@rm -Rf generated-docs
@purs docs ".spago/*/*/src/**/*.purs" --format html
clean:
rm -f dist/*
# inotifywait -m -r -q -e close_write --format '%T %w%f' --timefmt '%T' src | while read FILE; do echo $FILE; make; done

29
purs/README.md Normal file
View file

@ -0,0 +1,29 @@
## Development (Posix only)
1. Install `purescript`, `purescript-spago`, `terser`:
```
npm install
```
2. (optional) working with .dhall files:
```
stack install dhall dhall-json
```
3. Download purescript libraries (1x only):
```
make install
```
4. build dist/app.min.js:
```
make
```
On a successful build, `make` will also update `../static/js/`,
since the `purs/` folder is opaque to the espial executable build process.

1280
purs/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

15
purs/package.json Normal file
View file

@ -0,0 +1,15 @@
{
"name": "espial",
"private": true,
"scripts": {
"make-install": "make install",
"make-watch": "inotifywait -m -r -q -e close_write --format '%T %w%f' --timefmt '%T' src | while read FILE; do echo $FILE; make; done",
"make": "make"
},
"devDependencies": {
"purescript": "^0.12.1",
"purescript-spago": "^0.6.2",
"terser": "^3.14.1"
},
"dependencies": {}
}

121
purs/packages.dhall Normal file
View file

@ -0,0 +1,121 @@
{-
Welcome to Spacchetti local packages!
Below are instructions for how to edit this file for most use
cases, so that you don't need to know Dhall to use it.
## Warning: Don't Move This Top-Level Comment!
Due to how `dhall format` currently works, this comment's
instructions cannot appear near corresponding sections below
because `dhall format` will delete the comment. However,
it will not delete a top-level comment like this one.
## Use Cases
Most will want to do one or both of these options:
1. Override/Patch a package's dependency
2. Add a package not already in the default package set
This file will continue to work whether you use one or both options.
Instructions for each option are explained below.
### Overriding/Patching a package
Purpose:
- Change a package's dependency to a newer/older release than the
default package set's release
- Use your own modified version of some dependency that may
include new API, changed API, removed API by
using your custom git repo of the library rather than
the package set's repo
Syntax:
Replace the overrides' "{=}" (an empty record) with the following idea
The "//" or "⫽" means "merge these two records and
when they have the same value, use the one on the right:"
-------------------------------
let override =
{ packageName =
upstream.packageName ⫽ { updateEntity1 = "new value", updateEntity2 = "new value" }
, packageName =
upstream.packageName ⫽ { version = "v4.0.0" }
, packageName =
upstream.packageName // { repo = "https://www.example.com/path/to/new/repo.git" }
}
-------------------------------
Example:
-------------------------------
let overrides =
{ halogen =
upstream.halogen ⫽ { version = "master" }
, halogen-vdom =
upstream.halogen-vdom ⫽ { version = "v4.0.0" }
}
-------------------------------
### Additions
Purpose:
- Add packages that aren't alread included in the default package set
Syntax:
Replace the additions' "{=}" (an empty record) with the following idea:
-------------------------------
let additions =
{ "package-name" =
mkPackage
[ "dependency1"
, "dependency2"
]
"https://example.com/path/to/git/repo.git"
"tag ('v4.0.0') or branch ('master')"
, "package-name" =
mkPackage
[ "dependency1"
, "dependency2"
]
"https://example.com/path/to/git/repo.git"
"tag ('v4.0.0') or branch ('master')"
, etc.
}
-------------------------------
Example:
-------------------------------
let additions =
{ benchotron =
mkPackage
[ "arrays"
, "exists"
, "profunctor"
, "strings"
, "quickcheck"
, "lcg"
, "transformers"
, "foldable-traversable"
, "exceptions"
, "node-fs"
, "node-buffer"
, "node-readline"
, "datetime"
, "now"
]
"https://github.com/hdgarrood/purescript-benchotron.git"
"v7.0.0"
}
-------------------------------
-}
let mkPackage =
https://raw.githubusercontent.com/spacchetti/spacchetti/20190105/src/mkPackage.dhall sha256:8e1c6636f8a089f972b21cde0cef4b33fa36a2e503ad4c77928aabf92d2d4ec9
let upstream =
https://raw.githubusercontent.com/spacchetti/spacchetti/20190105/src/packages.dhall sha256:38fc3e19c193bb006c773ac84fc4a2888e5dcc610d36e49a9bdef7ecc7e1f8c9
let overrides = {=}
let additions = {=}
in upstream ⫽ overrides ⫽ additions

24
purs/spago.dhall Normal file
View file

@ -0,0 +1,24 @@
{ name =
"espial"
, dependencies =
[ "aff"
, "simple-json"
, "affjax"
, "argonaut"
, "arrays"
, "console"
, "debug"
, "effect"
, "either"
, "functions"
, "halogen"
, "prelude"
, "psci-support"
, "strings"
, "transformers"
, "web-html"
, "profunctor-lenses"
]
, packages =
./packages.dhall
}

119
purs/src/App.purs Normal file
View file

@ -0,0 +1,119 @@
module App where
import Prelude
import Affjax (Response, ResponseFormatError)
import Affjax (defaultRequest) as AX
import Affjax as Ax
import Affjax.RequestBody as AXReq
import Affjax.RequestHeader (RequestHeader(..))
import Affjax.ResponseFormat as AXRes
import Data.Argonaut (Json)
import Data.Array ((:))
import Data.Either (Either(..))
import Data.FormURLEncoded (FormURLEncoded)
import Data.HTTP.Method (Method(..))
import Data.Maybe (Maybe(..))
import Data.MediaType.Common (applicationFormURLEncoded, applicationJSON)
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Globals (app')
import Model (Bookmark, Bookmark'(..), Note, Note'(..), AccountSettings, AccountSettings'(..))
import Simple.JSON as J
import Web.HTML (window)
import Web.HTML.Location (reload)
import Web.HTML.Window (location)
data StarAction = Star | UnStar
instance showStar :: Show StarAction where
show Star = "star"
show UnStar = "unstar"
toggleStar :: Int -> StarAction -> Aff Unit
toggleStar bid action = do
let path = "bm/" <> show bid <> "/" <> show action
void (fetchUrlEnc POST path Nothing AXRes.ignore)
destroy :: Int -> Aff (Response (Either ResponseFormatError Unit))
destroy bid =
fetchUrlEnc DELETE ("bm/" <> show bid) Nothing AXRes.ignore
markRead :: Int -> Aff (Response (Either ResponseFormatError Unit))
markRead bid = do
let path = "bm/" <> show bid <> "/read"
fetchUrlEnc POST path Nothing AXRes.ignore
editBookmark :: Bookmark -> Aff (Response (Either ResponseFormatError Unit))
editBookmark bm = do
fetchJson POST "api/add" (Just (Bookmark' bm)) AXRes.ignore
editNote :: Note -> Aff (Response (Either ResponseFormatError Json))
editNote bm = do
fetchJson POST "api/note/add" (Just (Note' bm)) AXRes.json
destroyNote :: Int -> Aff (Response (Either ResponseFormatError Unit))
destroyNote nid = do
fetchUrlEnc DELETE ("api/note/" <> show nid) Nothing AXRes.ignore
editAccountSettings :: AccountSettings -> Aff (Response (Either ResponseFormatError Unit))
editAccountSettings us = do
fetchJson POST "api/accountSettings" (Just (AccountSettings' us)) AXRes.ignore
logout :: Unit -> Aff Unit
logout u = do
void (fetchUrl POST app.authRlogoutR [] Nothing AXRes.ignore)
liftEffect (window >>= location >>= reload)
where
app = app' u
fetchJson
:: forall a b.
J.WriteForeign b
=> Method
-> String
-> Maybe b
-> AXRes.ResponseFormat a
-> Aff (Response (Either ResponseFormatError a))
fetchJson method path content rt =
fetchPath method path [ContentType applicationJSON] (AXReq.string <<< J.writeJSON <$> content) rt
fetchUrlEnc
:: forall a.
Method
-> String
-> Maybe FormURLEncoded
-> AXRes.ResponseFormat a
-> Aff (Response (Either ResponseFormatError a))
fetchUrlEnc method path content rt =
fetchPath method path [ContentType applicationFormURLEncoded] (AXReq.FormURLEncoded <$> content) rt
fetchPath
:: forall a.
Method
-> String
-> Array RequestHeader
-> Maybe AXReq.RequestBody
-> AXRes.ResponseFormat a
-> Aff (Response (Either ResponseFormatError a))
fetchPath method path headers content rt =
fetchUrl method ((app' unit).homeR <> path) headers content rt
fetchUrl
:: forall a.
Method
-> String
-> Array RequestHeader
-> Maybe AXReq.RequestBody
-> AXRes.ResponseFormat a
-> Aff (Response (Either ResponseFormatError a))
fetchUrl method url headers content rt =
Ax.request
AX.defaultRequest
{ url = url
, method = Left method
, headers = RequestHeader app.csrfHeaderName app.csrfToken : headers
, content = content
, responseFormat = rt
}
where
app = app' unit

View file

@ -0,0 +1,91 @@
module Component.AccountSettings where
import Prelude hiding (div)
import App (editAccountSettings)
import Data.Lens (Lens', lens, use, (%=))
import Data.Maybe (Maybe(..))
import Effect.Aff (Aff)
import Globals (app')
import Halogen as H
import Halogen.HTML (HTML, div, input, text)
import Halogen.HTML.Elements (label)
import Halogen.HTML.Events (onChecked)
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties (InputType(..), checked, for, id_, name, type_)
import Model (AccountSettings)
import Util (class_)
import Web.Event.Event (Event)
type UState =
{ us :: AccountSettings
}
_us :: Lens' UState AccountSettings
_us = lens _.us (_ { us = _ })
data UQuery a
= UEditField EditField a
| USubmit Event a
data EditField
= EarchiveDefault Boolean
| EprivateDefault Boolean
| EprivacyLock Boolean
-- | The bookmark component definition.
usetting :: AccountSettings -> H.Component HTML UQuery Unit Unit Aff
usetting u' =
H.component
{ initialState: const (mkState u')
, render
, eval
, receiver: const Nothing
}
where
app = app' unit
mkState u =
{ us: u
}
render :: UState -> H.ComponentHTML UQuery
render { us } =
div [ class_ "settings-form" ]
[ div [ class_ "fw7 mb2"] [ text "Account Settings" ]
, div [ class_ "flex items-center mb2" ]
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id_ "archiveDefault", name "archiveDefault"
, checked (us.archiveDefault) , onChecked (editField EarchiveDefault) ]
, label [ for "archiveDefault", class_ "lh-copy" ]
[ text "Archive Non-Private Bookmarks (archive.li)" ]
]
, div [ class_ "flex items-center mb2" ]
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id_ "privateDefault", name "privateDefault"
, checked (us.privateDefault) , onChecked (editField EprivateDefault) ]
, label [ for "privateDefault", class_ "lh-copy" ]
[ text "Default new bookmarks to Private" ]
]
, div [ class_ "flex items-center mb2" ]
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id_ "privacyLock", name "privacyLock"
, checked (us.privacyLock) , onChecked (editField EprivacyLock) ]
, label [ for "privacyLock", class_ "lh-copy" ]
[ text "Privacy Lock (Private Account)" ]
]
]
where
editField :: forall a. (a -> EditField) -> a -> Maybe (UQuery Unit)
editField f = HE.input UEditField <<< f
eval :: UQuery ~> H.ComponentDSL UState UQuery Unit Aff
eval (UEditField f next) = do
_us %= case f of
EarchiveDefault e -> _ { archiveDefault = e }
EprivateDefault e -> _ { privateDefault = e }
EprivacyLock e -> _ { privacyLock = e }
pure next
eval (USubmit e next) = do
us <- use _us
void $ H.liftAff (editAccountSettings us)
pure next

179
purs/src/Component/Add.purs Normal file
View file

@ -0,0 +1,179 @@
module Component.Add where
import Prelude hiding (div)
import App (destroy, editBookmark)
import Data.Array (drop, foldMap)
import Data.Lens (Lens', lens, use, (%=), (.=))
import Data.Maybe (Maybe(..), maybe)
import Data.Monoid (guard)
import Data.String (null)
import Data.String (split) as S
import Data.String.Pattern (Pattern(..))
import Data.Tuple (fst, snd)
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Globals (app', closeWindow, mmoment8601)
import Halogen as H
import Halogen.HTML (HTML, br_, button, div, div_, form, input, label, p, span, table, tbody_, td, td_, text, textarea, tr_)
import Halogen.HTML.Events (onSubmit, onValueChange, onChecked, onClick)
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties (autofocus, ButtonType(..), InputType(..), autocomplete, checked, for, id_, name, required, rows, title, type_, value)
import Model (Bookmark)
import Util (_curQuerystring, _loc, _lookupQueryStringValue, attr, class_)
import Web.Event.Event (Event, preventDefault)
import Web.HTML (window)
import Web.HTML.Location (setHref)
data BQuery a
= BEditField EditField a
| BEditSubmit Event a
| BDeleteAsk Boolean a
| BDestroy a
data EditField
= Eurl String
| Etitle String
| Edescription String
| Etags String
| Eprivate Boolean
| Etoread Boolean
type BState =
{ bm :: Bookmark
, edit_bm :: Bookmark
, deleteAsk :: Boolean
, destroyed :: Boolean
}
_bm :: Lens' BState Bookmark
_bm = lens _.bm (_ { bm = _ })
_edit_bm :: Lens' BState Bookmark
_edit_bm = lens _.edit_bm (_ { edit_bm = _ })
addbmark :: Bookmark -> H.Component HTML BQuery Unit Unit Aff
addbmark b' =
H.component
{ initialState: const (mkState b')
, render
, eval
, receiver: const Nothing
}
where
app = app' unit
mkState b =
{ bm: b
, edit_bm: b
, deleteAsk: false
, destroyed: false
}
render :: BState -> H.ComponentHTML BQuery
render s@{ bm, edit_bm } =
div_ [ if not s.destroyed then display_edit else display_destroyed ]
where
display_edit =
form [ onSubmit (HE.input BEditSubmit) ]
[ table [ class_ "w-100" ]
[ tbody_
[ tr_
[ td [ class_ "w1" ] [ ]
, td_ $ guard (bm.bid > 0) [ display_exists ]
]
, tr_
[ td_ [ label [ for "url" ] [ text "URL" ] ]
, td_ [ input [ type_ InputUrl , id_ "url", class_ "w-100 mv1" , required true, name "url", autofocus (null bm.url)
, value (edit_bm.url) , onValueChange (editField Eurl)] ]
]
, tr_
[ td_ [ label [ for "title" ] [ text "title" ] ]
, td_ [ input [ type_ InputText , id_ "title", class_ "w-100 mv1" , name "title"
, value (edit_bm.title) , onValueChange (editField Etitle)] ]
]
, tr_
[ td_ [ label [ for "description" ] [ text "description" ] ]
, td_ [ textarea [ class_ "w-100 mt1 mid-gray" , id_ "description", name "description", rows 4
, value (edit_bm.description) , onValueChange (editField Edescription)] ]
]
, tr_
[ td_ [ label [ for "tags" ] [ text "tags" ] ]
, td_ [ input [ type_ InputText , id_ "tags", class_ "w-100 mv1" , name "tags", autocomplete false, attr "autocapitalize" "off", autofocus (not $ null bm.url)
, value (edit_bm.tags) , onValueChange (editField Etags)] ]
]
, tr_
[ td_ [ label [ for "private" ] [ text "private" ] ]
, td_ [ input [ type_ InputCheckbox , id_ "private", class_ "private pointer" , name "private"
, checked (edit_bm.private) , onChecked (editField Eprivate)] ]
]
, tr_
[ td_ [ label [ for "toread" ] [ text "read later" ] ]
, td_ [ input [ type_ InputCheckbox , id_ "toread", class_ "toread pointer" , name "toread"
, checked (edit_bm.toread) , onChecked (editField Etoread)] ]
]
, tr_
[ td_ [ ]
, td_ [ input [ type_ InputSubmit , class_ "ph3 pv2 input-reset ba b--navy bg-transparent pointer f6 dib mt1 dim"
, value (if bm.bid > 0 then "update bookmark" else "add bookmark") ] ]
]
]
]
]
display_exists =
div [ class_ "alert" ]
[ text "previously saved "
, span [ class_ "link f7 dib gray pr3" , title (maybe bm.time snd mmoment) ]
[ text (maybe " " fst mmoment) ]
, div [ class_ "edit_links dib ml1" ]
[ div [ class_ "delete_link di" ]
[ button ([ type_ ButtonButton, onClick (HE.input_ (BDeleteAsk true)), class_ "delete" ] <> guard s.deleteAsk [ attr "hidden" "hidden" ]) [ text "delete" ]
, span ([ class_ "confirm red" ] <> guard (not s.deleteAsk) [ attr "hidden" "hidden" ])
[ button [ type_ ButtonButton, onClick (HE.input_ (BDeleteAsk false))] [ text "cancel / " ]
, button [ type_ ButtonButton, onClick (HE.input_ BDestroy), class_ "red" ] [ text "destroy" ]
]
]
]
]
display_destroyed = p [ class_ "red"] [text "you killed this bookmark"]
editField :: forall a. (a -> EditField) -> a -> Maybe (BQuery Unit)
editField f = HE.input BEditField <<< f
mmoment = mmoment8601 bm.time
toTextarea =
drop 1
<<< foldMap (\x -> [br_, text x])
<<< S.split (Pattern "\n")
eval :: BQuery ~> H.ComponentDSL BState BQuery Unit Aff
eval (BDeleteAsk e next) = do
H.modify_ (_ { deleteAsk = e })
pure next
eval (BDestroy next) = do
bid <- H.gets _.bm.bid
void $ H.liftAff (destroy bid)
H.modify_ (_ { destroyed = true })
pure next
eval (BEditField f next) = do
_edit_bm %= case f of
Eurl e -> _ { url = e }
Etitle e -> _ { title = e }
Edescription e -> _ { description = e }
Etags e -> _ { tags = e }
Eprivate e -> _ { private = e }
Etoread e -> _ { toread = e }
pure next
eval (BEditSubmit e next) = do
H.liftEffect (preventDefault e)
edit_bm <- use _edit_bm
void $ H.liftAff (editBookmark edit_bm)
_bm .= edit_bm
loc <- liftEffect _loc
win <- liftEffect window
qs <- liftEffect _curQuerystring
case _lookupQueryStringValue qs "next" of
Just n -> liftEffect (setHref n loc)
_ -> liftEffect (closeWindow win)
pure next

View file

@ -0,0 +1,48 @@
module Component.BList where
import Prelude
import Component.BMark (BMessage(..), BQuery, bmark)
import Model (Bookmark, BookmarkId)
import Data.Array (filter)
import Data.Maybe (Maybe(..))
import Effect.Aff (Aff)
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
type BSlot = BookmarkId
data LQuery a =
HandleBMessage BSlot BMessage a
blist :: Array Bookmark -> H.Component HH.HTML LQuery Unit Void Aff
blist st =
H.parentComponent
{ initialState: const st
, render
, eval
, receiver: const Nothing
}
where
render :: Array Bookmark -> H.ParentHTML LQuery BQuery BSlot Aff
render bms =
HH.div_ (map renderBookmark bms)
where
renderBookmark :: Bookmark -> H.ParentHTML LQuery BQuery BSlot Aff
renderBookmark b =
HH.slot
b.bid
(bmark b)
unit
(HE.input (HandleBMessage b.bid))
eval :: LQuery ~> H.ParentDSL (Array Bookmark) LQuery BQuery BSlot Void Aff
eval (HandleBMessage p BNotifyRemove next) = do
H.modify_ (removeBookmark p)
pure next
where
removeBookmark :: BookmarkId -> Array Bookmark -> Array Bookmark
removeBookmark bookmarkId = filter (\b -> b.bid /= bookmarkId)

View file

@ -0,0 +1,247 @@
module Component.BMark where
import Prelude hiding (div)
import App (StarAction(..), destroy, editBookmark, markRead, toggleStar)
import Data.Array (drop, foldMap)
import Data.Lens (Lens', lens, use, (%=), (.=))
import Data.Maybe (Maybe(..), fromMaybe, isJust, maybe)
import Data.Monoid (guard)
import Data.Nullable (toMaybe)
import Data.String (null, split, take) as S
import Data.String.Pattern (Pattern(..))
import Data.Tuple (fst, snd)
import Effect.Aff (Aff)
import Globals (app', mmoment8601)
import Halogen as H
import Halogen.HTML (HTML, a, br_, button, div, div_, form, input, label, span, text, textarea)
import Halogen.HTML.Events (onSubmit, onValueChange, onChecked, onClick)
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties (ButtonType(..), InputType(..), autocomplete, checked, for, href, id_, name, required, rows, target, title, type_, value)
import Model (Bookmark)
import Util (class_, attr, fromNullableStr)
import Web.Event.Event (Event, preventDefault)
-- | UI Events
data BQuery a
= BStar Boolean a
| BDeleteAsk Boolean a
| BDestroy a
| BEdit Boolean a
| BEditField EditField a
| BEditSubmit Event a
| BMarkRead a
-- | FormField Edits
data EditField
= Eurl String
| Etitle String
| Edescription String
| Etags String
| Eprivate Boolean
| Etoread Boolean
-- | Messages to parent
data BMessage
= BNotifyRemove
type BState =
{ bm :: Bookmark
, edit_bm :: Bookmark
, deleteAsk:: Boolean
, edit :: Boolean
}
_bm :: Lens' BState Bookmark
_bm = lens _.bm (_ { bm = _ })
_edit_bm :: Lens' BState Bookmark
_edit_bm = lens _.edit_bm (_ { edit_bm = _ })
_edit :: Lens' BState Boolean
_edit = lens _.edit (_ { edit = _ })
bmark :: Bookmark -> H.Component HTML BQuery Unit BMessage Aff
bmark b' =
H.component
{ initialState: const (mkState b')
, render
, eval
, receiver: const Nothing
}
where
app = app' unit
mkState b =
{ bm: b
, edit_bm: b
, deleteAsk: false
, edit: false
}
render :: BState -> H.ComponentHTML BQuery
render s@{ bm, edit_bm } =
div [ id_ (show bm.bid) , class_ ("bookmark w-100 mw7 pa1 mb3" <> guard bm.private " private")] $
star <>
if s.edit
then display_edit
else display
where
star =
guard app.dat.isowner
[ div [ class_ ("star fl pointer" <> guard bm.selected " selected") ]
[ button [ class_ "moon-gray", onClick (HE.input_ (BStar (not bm.selected))) ] [ text "✭" ] ]
]
display =
[ div [ class_ "display" ] $
[ a [ href bm.url, target "_blank", class_ ("link f5 lh-title" <> guard bm.toread " unread")]
[ text $ if S.null bm.title then "[no title]" else bm.title ]
, br_
, a [ href bm.url , class_ "link f7 gray hover-blue" ] [ text bm.url ]
, a [ href (fromMaybe ("http://archive.is/" <> bm.url) (toMaybe bm.archiveUrl))
, class_ ("link f7 gray hover-blue ml2" <> (guard (isJust (toMaybe bm.archiveUrl)) " green"))
, target "_blank", title "archive link"]
[ if isJust (toMaybe bm.archiveUrl) then text "☑" else text "☐" ]
, br_
--
, div [ class_ "description mt1 mid-gray" ] (toTextarea bm.description)
, div [ class_ "tags" ] $
guard (not (S.null bm.tags))
map (\tag -> a [ class_ ("link tag mr1" <> guard (S.take 1 tag == ".") " private")
, href (linkToFilterTag tag) ]
[ text tag ])
(S.split (Pattern " ") bm.tags)
, a [ class_ "link f7 dib gray w4", title (maybe bm.time snd mmoment) , href (linkToFilterSingle bm.slug) ]
[ text (maybe " " fst mmoment) ]
]
<> links
]
display_edit =
[ div [ class_ "edit_bookmark_form pa2 pt0 bg-white" ] $
[ form [ onSubmit (HE.input BEditSubmit) ]
[ div_ [ text "url" ]
, input [ type_ InputUrl , class_ "url w-100 mb2 pt1 f7 edit_form_input" , required true , name "url"
, value (edit_bm.url) , onValueChange (editField Eurl) ]
, br_
, div_ [ text "title" ]
, input [ type_ InputText , class_ "title w-100 mb2 pt1 f7 edit_form_input" , name "title"
, value (edit_bm.title) , onValueChange (editField Etitle) ]
, br_
, div_ [ text "description" ]
, textarea [ class_ "description w-100 mb1 pt1 f7 edit_form_input" , name "description", rows 5
, value (edit_bm.description) , onValueChange (editField Edescription) ]
, br_
, div [ id_ "tags_input_box"]
[ div_ [ text "tags" ]
, input [ type_ InputText , class_ "tags w-100 mb1 pt1 f7 edit_form_input" , name "tags"
, autocomplete false, attr "autocapitalize" "off"
, value (edit_bm.tags) , onValueChange (editField Etags) ]
, br_
]
, div [ class_ "edit_form_checkboxes mv3"]
[ input [ type_ InputCheckbox , class_ "private pointer" , id_ "edit_private", name "private"
, checked (edit_bm.private) , onChecked (editField Eprivate) ]
, text " "
, label [ for "edit_private" , class_ "mr2" ] [ text "private" ]
, text " "
, input [ type_ InputCheckbox , class_ "toread pointer" , id_ "edit_toread", name "toread"
, checked (edit_bm.toread) , onChecked (editField Etoread) ]
, text " "
, label [ for "edit_toread" ] [ text "to-read" ]
, br_
]
, input [ type_ InputSubmit , class_ "mr1 pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "save" ]
, text " "
, input [ type_ InputReset , class_ "pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "cancel"
, onClick (HE.input_ (BEdit false)) ]
]
]
]
links =
guard app.dat.isowner
[ div [ class_ "edit_links di" ]
[ button [ type_ ButtonButton, onClick (HE.input_ (BEdit true)), class_ "edit light-silver hover-blue" ] [ text "edit  " ]
, div [ class_ "delete_link di" ]
[ button [ type_ ButtonButton, onClick (HE.input_ (BDeleteAsk true)), class_ ("delete light-silver hover-blue" <> guard s.deleteAsk " dn") ] [ text "delete" ]
, span ([ class_ ("confirm red" <> guard (not s.deleteAsk) " dn") ] )
[ button [ type_ ButtonButton, onClick (HE.input_ (BDeleteAsk false))] [ text "cancel / " ]
, button [ type_ ButtonButton, onClick (HE.input_ BDestroy), class_ "red" ] [ text "destroy" ]
]
]
]
, div [ class_ "read di" ] $
guard bm.toread
[ text "  "
, button [ onClick (HE.input_ BMarkRead), class_ "mark_read" ] [ text "mark as read"]
]
]
editField :: forall a. (a -> EditField) -> a -> Maybe (BQuery Unit)
editField f = HE.input BEditField <<< f
linkToFilterSingle slug = fromNullableStr app.userR <> "/b:" <> slug
linkToFilterTag tag = fromNullableStr app.userR <> "/t:" <> tag
mmoment = mmoment8601 bm.time
toTextarea input =
S.split (Pattern "\n") input
# foldMap (\x -> [br_, text x])
# drop 1
eval :: BQuery ~> H.ComponentDSL BState BQuery BMessage Aff
-- | Star
eval (BStar e next) = do
bm <- use _bm
H.liftAff (toggleStar bm.bid (if e then Star else UnStar))
_bm %= _ { selected = e }
_edit_bm %= _ { selected = e }
pure next
-- | Delete
eval (BDeleteAsk e next) = do
H.modify_ (_ { deleteAsk = e })
pure next
-- | Destroy
eval (BDestroy next) = do
bm <- use _bm
void $ H.liftAff (destroy bm.bid)
H.raise BNotifyRemove
pure next
-- | Mark Read
eval (BMarkRead next) = do
bm <- use _bm
void (H.liftAff (markRead bm.bid))
_bm %= _ { toread = false }
pure next
-- | Start/Stop Editing
eval (BEdit e next) = do
bm <- use _bm
_edit_bm .= bm
_edit .= e
pure next
-- | Update Form Field
eval (BEditField f next) = do
_edit_bm %= case f of
Eurl e -> _ { url = e }
Etitle e -> _ { title = e }
Edescription e -> _ { description = e }
Etags e -> _ { tags = e }
Eprivate e -> _ { private = e }
Etoread e -> _ { toread = e }
pure next
-- | Submit
eval (BEditSubmit e next) = do
H.liftEffect (preventDefault e)
edit_bm <- use _edit_bm
void $ H.liftAff (editBookmark edit_bm)
_bm .= edit_bm
_edit .= false
pure next

View file

@ -0,0 +1,15 @@
module Component.Markdown (component, MInput, MQuery, MOutput, module RHExt) where
import Component.RawHtml as RH
import Component.RawHtml (Query(Receive)) as RHExt
import Effect.Aff (Aff)
import Foreign.Marked (marked)
import Halogen as H
import Halogen.HTML as HH
type MInput = String
type MQuery = RH.Query String
type MOutput = RH.Output
component :: H.Component HH.HTML MQuery MInput MOutput Aff
component = RH.mkComponent marked

View file

@ -0,0 +1,75 @@
module Component.NList where
import Prelude hiding (div)
import Data.Array (drop, foldMap)
import Data.Maybe (Maybe(..), maybe)
import Data.String (null, split, take) as S
import Data.String.Pattern (Pattern(..))
import Data.Tuple (fst, snd)
import Effect.Aff (Aff)
import Globals (app', mmoment8601)
import Halogen as H
import Halogen.HTML (a, br_, div, text)
import Halogen.HTML as HH
import Halogen.HTML.Properties (href, id_, title)
import Model (Note, NoteSlug)
import Util (class_, fromNullableStr)
data NLQuery a
= NLNop a
type NLSlot = NoteSlug
type NLState =
{ notes :: Array Note
, cur :: Maybe NLSlot
, deleteAsk:: Boolean
, edit :: Boolean
}
nlist :: Array Note -> H.Component HH.HTML NLQuery Unit Void Aff
nlist st' =
H.component
{ initialState: const (mkState st')
, render
, eval
, receiver: const Nothing
}
where
app = app' unit
mkState notes' =
{ notes: notes'
, cur: Nothing
, deleteAsk: false
, edit: false
}
render :: NLState -> H.ComponentHTML NLQuery
render st@{ notes } =
HH.div_ (map renderNote notes)
where
renderNote :: Note -> H.ComponentHTML NLQuery
renderNote bm =
div [ id_ (show bm.id) , class_ ("note w-100 mw7 pa1 mb2")] $
[ div [ class_ "display" ] $
[ a [ href (linkToFilterSingle bm.slug), class_ ("link f5 lh-title")]
[ text $ if S.null bm.title then "[no title]" else bm.title ]
, br_
, div [ class_ "description mt1 mid-gray" ] (toTextarea (S.take 200 bm.text))
, a [ class_ "link f7 dib gray w4", title (maybe bm.created snd (mmoment bm)) , href (linkToFilterSingle bm.slug) ]
[ text (maybe " " fst (mmoment bm)) ]
]
]
mmoment bm = mmoment8601 bm.created
linkToFilterSingle slug = fromNullableStr app.userR <> "/notes/" <> slug
toTextarea input =
S.split (Pattern "\n") input
# foldMap (\x -> [br_, text x])
# drop 1
eval :: NLQuery ~> H.ComponentDSL NLState NLQuery Void Aff
eval (NLNop next) = pure next

View file

@ -0,0 +1,197 @@
module Component.NNote where
import Prelude hiding (div)
import App (destroyNote, editNote)
import Component.Markdown as Markdown
import Data.Array (drop, foldMap)
import Data.Either (Either(..))
import Data.Lens (Lens', lens, use, (%=), (.=))
import Data.Maybe (Maybe(..), maybe)
import Data.Monoid (guard)
import Data.String (null, split) as S
import Data.String.Pattern (Pattern(..))
import Data.Tuple (fst, snd)
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Globals (app', mmoment8601)
import Halogen as H
import Halogen.HTML (br_, button, div, form, input, label, p, span, text, textarea)
import Halogen.HTML as HH
import Halogen.HTML.Events (onChecked, onClick, onSubmit, onValueChange)
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties (ButtonType(..), InputType(..), checked, for, id_, name, rows, title, type_, value)
import Model (Note)
import Util (_loc, class_, fromNullableStr)
import Web.Event.Event (Event, preventDefault)
import Web.HTML.Location (setHref)
data NQuery a
= NNop a
| NEditField EditField a
| NEditSubmit Event a
| NEdit Boolean a
| NDeleteAsk Boolean a
| NDestroy a
type NState =
{ note :: Note
, edit_note :: Note
, deleteAsk :: Boolean
, edit :: Boolean
, destroyed :: Boolean
}
_note :: Lens' NState Note
_note = lens _.note (_ { note = _ })
_edit_note :: Lens' NState Note
_edit_note = lens _.edit_note (_ { edit_note = _ })
_edit :: Lens' NState Boolean
_edit = lens _.edit (_ { edit = _ })
-- | FormField Edits
data EditField
= Etitle String
| Etext String
| EisMarkdown Boolean
type NChildQuery = Markdown.MQuery
nnote :: Note -> H.Component HH.HTML NQuery Unit Void Aff
nnote st' =
H.parentComponent
{ initialState: const (mkState st')
, render
, eval
, receiver: const Nothing
}
where
app = app' unit
mkState note' =
{ note: note'
, edit_note: note'
, deleteAsk: false
, edit: note'.id <= 0
, destroyed: false
}
render :: NState -> H.ParentHTML NQuery NChildQuery Unit Aff
render st@{ note, edit_note } =
if st.destroyed
then display_destroyed
else
if st.edit
then renderNote_edit
else renderNote
where
renderNote =
div [ id_ (show note.id) , class_ ("note w-100 mw7 pa1 mb2")] $
[ div [ class_ "display" ] $
[ div [ class_ ("link f5 lh-title")]
[ text $ if S.null note.title then "[no title]" else note.title ]
, br_
, if note.isMarkdown
then div [ class_ "description mt1" ] [ HH.slot unit Markdown.component note.text absurd ]
else div [ class_ "description mt1 mid-gray" ] (toTextarea note.text)
, div [ class_ "link f7 dib gray w4", title (maybe note.created snd (mmoment note)) ]
[ text (maybe " " fst (mmoment note)) ]
]
]
<> -- | Render Action Links
[ div [ class_ "edit_links db mt3" ]
[ button [ type_ ButtonButton, onClick (HE.input_ (NEdit true)), class_ "edit light-silver hover-blue" ] [ text "edit  " ]
, div [ class_ "delete_link di" ]
[ button [ type_ ButtonButton, onClick (HE.input_ (NDeleteAsk true)), class_ ("delete light-silver hover-blue" <> guard st.deleteAsk " dn") ] [ text "delete" ]
, span ([ class_ ("confirm red" <> guard (not st.deleteAsk) " dn") ] )
[ button [ type_ ButtonButton, onClick (HE.input_ (NDeleteAsk false))] [ text "cancel / " ]
, button [ type_ ButtonButton, onClick (HE.input_ NDestroy), class_ "red" ] [ text "destroy" ]
]
]
]
]
renderNote_edit =
form [ onSubmit (HE.input NEditSubmit) ]
[ p [ class_ "mt2 mb1"] [ text "title:" ]
, input [ type_ InputText , class_ "title w-100 mb1 pt1 f7 edit_form_input" , name "title"
, value (edit_note.title) , onValueChange (editField Etitle)
]
, br_
, p [ class_ "mt2 mb1"] [ text "description:" ]
, textarea [ class_ "description w-100 mb1 pt1 f7 edit_form_input" , name "text", rows 30
, value (edit_note.text) , onValueChange (editField Etext)
]
, div [ class_ "edit_form_checkboxes mb3"]
[ input [ type_ InputCheckbox , class_ "is-markdown pointer" , id_ "edit_ismarkdown", name "ismarkdown"
, checked (edit_note.isMarkdown) , onChecked (editField EisMarkdown) ]
, text " "
, label [ for "edit_ismarkdown" , class_ "mr2" ] [ text "use markdown?" ]
, br_
]
, input [ type_ InputSubmit , class_ "mr1 pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "save" ]
, text " "
, input [ type_ InputReset , class_ "pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "cancel"
, onClick (HE.input_ (NEdit false))
]
]
display_destroyed = p [ class_ "red"] [text "you killed this note"]
mmoment n = mmoment8601 n.created
editField :: forall a. (a -> EditField) -> a -> Maybe (NQuery Unit)
editField f = HE.input NEditField <<< f
toTextarea input =
S.split (Pattern "\n") input
# foldMap (\x -> [br_, text x])
# drop 1
eval :: NQuery ~> H.ParentDSL NState NQuery NChildQuery Unit Void Aff
eval (NNop next) = pure next
-- | EditField
eval (NEditField f next) = do
_edit_note %= case f of
Etitle e -> _ { title = e }
Etext e -> _ { text = e }
EisMarkdown e -> _ { isMarkdown = e }
pure next
-- | Delete
eval (NDeleteAsk e next) = do
H.modify_ (_ { deleteAsk = e })
pure next
-- | Destroy
eval (NDestroy next) = do
note <- use _note
void $ H.liftAff (destroyNote note.id)
H.modify_ (_ { destroyed = true })
pure next
-- | Start/Stop Editing
eval (NEdit e next) = do
note <- use _note
_edit_note .= note
_edit .= e
pure next
-- | Submit
eval (NEditSubmit e next) = do
H.liftEffect (preventDefault e)
edit_note <- use _edit_note
res <- H.liftAff (editNote edit_note)
case res.body of
Left err -> pure next
Right r -> do
if (edit_note.id == 0)
then do
liftEffect (setHref (fromNullableStr app.noteR) =<< _loc)
else do
_note .= edit_note
_edit .= false
pure next

View file

@ -0,0 +1,8 @@
// use at your own risk!
exports.unsafeSetInnerHTML = function(element) {
return function(html) {
return function() {
element.innerHTML = html;
};
};
};

View file

@ -0,0 +1,62 @@
module Component.RawHtml where
import Prelude
import Data.Foldable (for_)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Aff (Aff)
import Globals (RawHTML(..))
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Web.HTML (HTMLElement)
foreign import unsafeSetInnerHTML :: HTMLElement -> RawHTML -> Effect Unit
data Query i a
= SetInnerHTML a
| Receive (Input i) a
type Input i = i
type Output = Void
type State i =
{ elRef :: H.RefLabel
, inputval :: Input i
}
component :: H.Component HH.HTML (Query String) (Input String) Output Aff
component = mkComponent RawHTML
mkComponent :: forall i. (Input i -> RawHTML) -> H.Component HH.HTML (Query i) (Input i) Output Aff
mkComponent toRawHTML = H.lifecycleComponent
{ initialState: \inputval -> { elRef: H.RefLabel "inputval", inputval }
, render
, eval
, receiver: HE.input Receive
, initializer: Just $ H.action SetInnerHTML
, finalizer: Nothing
}
where
render :: (State i) -> H.ComponentHTML (Query i)
render state =
HH.div
[ HP.ref state.elRef ]
[]
eval :: (Query i) ~> H.ComponentDSL (State i) (Query i) Output Aff
eval = case _ of
SetInnerHTML a -> do
{ elRef } <- H.get
mel <- H.getHTMLElementRef elRef
for_ mel \el -> do
{ inputval } <- H.get
H.liftEffect (unsafeSetInnerHTML el (toRawHTML inputval))
pure a
Receive inputval a -> do
H.modify_ _ { inputval = inputval }
eval $ SetInnerHTML a

65
purs/src/Globals.js Normal file
View file

@ -0,0 +1,65 @@
"use strict";
exports._app = function() {
return app;
}
exports._closest = function(just, nothing, selector, el) {
var node = el.closest(selector);
if(node) {
return just(node);
} else {
return nothing;
}
}
exports._innerHtml = function(el) {
return el.innerHTML;
}
exports._setInnerHtml = function(content, el) {
el.innerHTML = content;
return el;
}
exports._createFormData = function(formElement) {
return new FormData(formElement);
}
exports._createFormString = function(formElement) {
return new URLSearchParams(new FormData(formElement)).toString()
}
exports._createFormArray = function(formElement) {
return Array.from(new FormData(formElement));
}
exports._getDataAttribute = function(name, el) {
return el.dataset[name];
}
exports._setDataAttribute = function(name, value, el) {
return el.dataset[name] = value;
}
exports._moment8601 = function(tuple, s) {
var m = moment(s, moment.ISO_8601);
var s1 = m.fromNow();
var s2 = m.format('MMMM D YYYY, h:mm a') + " (" + m.format() + ") ";
return tuple(s1)(s2);
}
exports._mmoment8601 = function(just, nothing, tuple, s) {
try {
var m = moment(s, moment.ISO_8601);
var s1 = m.fromNow();
var s2 = m.format('MMMM D YYYY, h:mm a') + " (" + m.format() + ") ";
return just(tuple(s1)(s2));
} catch (error) {
return nothing
}
}
exports._closeWindow = function (window) {
window.close();
};

97
purs/src/Globals.purs Normal file
View file

@ -0,0 +1,97 @@
module Globals where
import Data.Function.Uncurried
import Data.Maybe (Maybe(..))
import Data.Nullable (Nullable, toMaybe)
import Data.Tuple (Tuple(..))
import Effect (Effect)
import Model (Bookmark)
import Prelude (Unit, pure, ($))
import Web.DOM (Element, Node)
import Web.HTML (HTMLElement, HTMLFormElement, Window)
import Web.XHR.FormData (FormData)
import Data.Newtype (class Newtype)
type App =
{ csrfHeaderName :: String
, csrfCookieName :: String
, csrfParamName :: String
, csrfToken :: String
, homeR :: String
, authRlogoutR :: String
, userR :: Nullable String
, noteR :: Nullable String
, dat :: AppData
}
type AppData =
{ bmarks :: Array Bookmark
, bmark :: Bookmark
, isowner :: Boolean
}
foreign import _app :: Fn0 App
app' :: Unit -> App
app' _ = runFn0 _app
foreign import _closest :: forall a. Fn4 (a -> Maybe a) (Maybe a) String Node (Maybe Node)
closest :: String -> Node -> Effect (Maybe Node)
closest selector node = pure $ runFn4 _closest Just Nothing selector node
foreign import _moment8601 :: Fn2 (String -> String -> Tuple String String) String (Tuple String String)
moment8601 :: String -> Effect (Tuple String String)
moment8601 s = pure $ runFn2 _moment8601 Tuple s
foreign import _mmoment8601 :: forall a. Fn4 (a -> Maybe a) (Maybe a) (String -> String -> Tuple String String) String (Maybe (Tuple String String))
mmoment8601 :: String -> Maybe (Tuple String String)
mmoment8601 s = runFn4 _mmoment8601 Just Nothing Tuple s
foreign import _innerHtml :: Fn1 HTMLElement String
innerHtml :: HTMLElement -> Effect String
innerHtml n = pure $ runFn1 _innerHtml n
foreign import _setInnerHtml :: Fn2 String HTMLElement HTMLElement
setInnerHtml :: String -> HTMLElement -> Effect HTMLElement
setInnerHtml c n = pure $ runFn2 _setInnerHtml c n
foreign import _createFormData :: Fn1 HTMLFormElement FormData
createFormData :: HTMLFormElement -> FormData
createFormData f = runFn1 _createFormData f
foreign import _createFormString :: Fn1 HTMLFormElement String
createFormString :: HTMLFormElement -> String
createFormString f = runFn1 _createFormString f
foreign import _createFormArray :: Fn1 HTMLFormElement (Array (Array String))
createFormArray :: HTMLFormElement -> (Array (Array String))
createFormArray f = runFn1 _createFormArray f
foreign import _getDataAttribute :: Fn2 String Element (Nullable String)
getDataAttribute :: String -> Element -> Effect (Maybe String)
getDataAttribute k n = pure $ toMaybe $ runFn2 _getDataAttribute k n
foreign import _setDataAttribute :: Fn3 String String Element Unit
setDataAttribute :: String -> String -> Element -> Effect Unit
setDataAttribute k v n = pure $ runFn3 _setDataAttribute k v n
foreign import _closeWindow :: Fn1 Window Unit
closeWindow :: Window -> Effect Unit
closeWindow win = pure $ runFn1 _closeWindow win
newtype RawHTML = RawHTML String
derive instance newtypeRawHTML :: Newtype RawHTML _

63
purs/src/Main.purs Normal file
View file

@ -0,0 +1,63 @@
module Main where
import Prelude
import App (logout)
import Component.Add (addbmark)
import Component.BList (blist)
import Component.NList (nlist)
import Component.NNote (nnote)
import Component.AccountSettings (usetting)
import Data.Foldable (traverse_)
import Effect (Effect)
import Effect.Aff (Aff, launchAff)
import Effect.Class (liftEffect)
import Halogen.Aff as HA
import Halogen.VDom.Driver (runUI)
import Model (Bookmark, Note, AccountSettings)
import Web.DOM.Element (removeAttribute)
import Web.DOM.ParentNode (QuerySelector(..))
import Web.Event.Event (Event, preventDefault)
import Web.HTML.HTMLElement (toElement)
main :: Effect Unit
main = pure unit
logoutE :: Event -> Effect Unit
logoutE e = void <<< launchAff <<< logout =<< preventDefault e
renderBookmarks :: String -> Array Bookmark -> Effect Unit
renderBookmarks renderElSelector bmarks = do
HA.runHalogenAff do
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
void $ runUI (blist bmarks) unit el
showFooter
renderAddForm :: String -> Bookmark -> Effect Unit
renderAddForm renderElSelector bmark = do
HA.runHalogenAff do
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
runUI (addbmark bmark) unit el
renderNotes :: String -> Array Note -> Effect Unit
renderNotes renderElSelector notes = do
HA.runHalogenAff do
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
void $ runUI (nlist notes) unit el
showFooter
renderNote :: String -> Note -> Effect Unit
renderNote renderElSelector note = do
HA.runHalogenAff do
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
void $ runUI (nnote note) unit el
renderAccountSettings :: String -> AccountSettings -> Effect Unit
renderAccountSettings renderElSelector accountSettings = do
HA.runHalogenAff do
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
void $ runUI (usetting accountSettings) unit el
showFooter :: Aff Unit
showFooter = HA.selectElement (QuerySelector ".user_footer") >>= traverse_ \el ->
liftEffect $ removeAttribute "hidden" (toElement el)

7
purs/src/Marked.js Normal file
View file

@ -0,0 +1,7 @@
exports.markedImpl = function(str) {
marked.setOptions({
pedantic: false,
gfm: true
});
return marked(str);
};

9
purs/src/Marked.purs Normal file
View file

@ -0,0 +1,9 @@
module Foreign.Marked where
import Prelude
import Globals (RawHTML(..))
foreign import markedImpl :: String -> String
marked :: String -> RawHTML
marked = RawHTML <<< markedImpl

53
purs/src/Model.purs Normal file
View file

@ -0,0 +1,53 @@
module Model where
import Data.Nullable (Nullable)
import Simple.JSON as J
type BookmarkId = Int
type TagId = Int
type Bookmark =
{ url :: String
, title :: String
, description :: String
, tags :: String
, private :: Boolean
, toread :: Boolean
, bid :: BookmarkId
, slug :: String
, selected :: Boolean
, time :: String
, archiveUrl :: Nullable String
}
newtype Bookmark' = Bookmark' Bookmark
derive newtype instance bookmark_rfI :: J.ReadForeign Bookmark'
derive newtype instance bookmark_wfI :: J.WriteForeign Bookmark'
type NoteId = Int
type NoteSlug = String
type Note =
{ id :: NoteId
, slug :: NoteSlug
, title :: String
, text :: String
, length :: Int
, isMarkdown :: Boolean
, created :: String
, updated :: String
}
newtype Note' = Note' Note
derive newtype instance note_rfI :: J.ReadForeign Note'
derive newtype instance note_wfI :: J.WriteForeign Note'
type AccountSettings =
{ archiveDefault :: Boolean
, privateDefault :: Boolean
, privacyLock :: Boolean
}
newtype AccountSettings' = AccountSettings' AccountSettings
derive newtype instance usersettings_rfI :: J.ReadForeign AccountSettings'
derive newtype instance usersettings_wfI :: J.WriteForeign AccountSettings'

136
purs/src/Util.purs Normal file
View file

@ -0,0 +1,136 @@
module Util where
import Prelude
import Control.Monad.Maybe.Trans (MaybeT(..))
import Data.Array (filter, find, mapMaybe)
import Data.Foldable (for_)
import Data.Maybe (Maybe(..), fromJust, fromMaybe, maybe)
import Data.Nullable (Nullable, toMaybe)
import Data.String (Pattern(..), Replacement(..), drop, replaceAll, split, take)
import Data.Tuple (Tuple(..), fst, snd)
import Effect (Effect)
import Global.Unsafe (unsafeDecodeURIComponent)
import Halogen (ClassName(..))
import Halogen.HTML as HH
import Halogen.HTML.Properties as HP
import Partial.Unsafe (unsafePartial)
import Web.DOM (Element, Node)
import Web.DOM.Document (toNonElementParentNode)
import Web.DOM.Element (fromNode, toParentNode)
import Web.DOM.NodeList (toArray)
import Web.DOM.NonElementParentNode (getElementById)
import Web.DOM.ParentNode (QuerySelector(..), querySelector, querySelectorAll)
import Web.HTML (HTMLDocument, Location, window)
import Web.HTML.HTMLDocument (body) as HD
import Web.HTML.HTMLDocument (toDocument)
import Web.HTML.HTMLElement (HTMLElement)
import Web.HTML.HTMLElement (fromElement) as HE
import Web.HTML.Location (search)
import Web.HTML.Window (document, location)
-- Halogen
class_ :: forall r i. String -> HP.IProp ( "class" :: String | r) i
class_ = HP.class_ <<< HH.ClassName
attr :: forall r i. String -> String -> HP.IProp r i
attr a = HP.attr (HH.AttrName a)
-- Util
_queryBoth :: forall a. Tuple String Element -> Tuple String Element -> (Element -> Element -> Effect a) -> Effect Unit
_queryBoth (Tuple qa ea) (Tuple qb eb) f = do
ma <- _querySelector qa ea
mb <- _querySelector qb eb
for_ ma \a ->
for_ mb \b ->
f a b
_queryBoth' :: forall a. Tuple String Element -> Tuple String Element -> (Element -> Array Node -> Effect a) -> Effect Unit
_queryBoth' (Tuple qa ea) (Tuple qb eb) f = do
ma <- _querySelector qa ea
bs <- _querySelectorAll qb eb
for_ ma \a ->
f a bs
_queryBoth'' :: forall a. Tuple String Element -> Tuple String Element -> (Array Node -> Array Node -> Effect a) -> Effect a
_queryBoth'' (Tuple qa ea) (Tuple qb eb) f = do
as <- _querySelectorAll qa ea
bs <- _querySelectorAll qb eb
f as bs
_querySelector :: String -> Element -> Effect (Maybe Element)
_querySelector s n = querySelector (QuerySelector s) (toParentNode n)
_querySelectorAll :: String -> Element -> Effect (Array Node)
_querySelectorAll s n = toArray =<< querySelectorAll (QuerySelector s) (toParentNode n)
_fromNode :: Node -> Element
_fromNode e = unsafePartial $ fromJust (fromNode e)
_fromElement :: Element -> HTMLElement
_fromElement e = unsafePartial $ fromJust (HE.fromElement e)
_getElementById :: String -> HTMLDocument -> Effect (Maybe Element)
_getElementById s = getElementById s <<< toNonElementParentNode <<< toDocument
_doc :: Effect HTMLDocument
_doc = document =<< window
_loc :: Effect Location
_loc = location =<< window
type QueryStringArray = Array (Tuple String (Maybe String))
_curQuerystring :: Effect QueryStringArray
_curQuerystring = do
loc <- _loc
srh <- search loc
pure $ _parseQueryString srh
_parseQueryString :: String -> QueryStringArray
_parseQueryString srh = do
let qs = let srh' = take 1 srh in if (srh' == "#" || srh' == "?") then drop 1 srh else srh
mapMaybe go $ (filter (_ /= "") <<< split (Pattern "&")) qs
where
decode = unsafeDecodeURIComponent <<< replaceAll (Pattern "+") (Replacement " ")
go kv =
case split (Pattern "=") kv of
[k] -> Just (Tuple (decode k) Nothing)
[k, v] -> Just (Tuple (decode k) (Just (decode v)))
_ -> Nothing
_lookupQueryStringValue :: QueryStringArray -> String -> Maybe String
_lookupQueryStringValue qs k = do
join $ map snd $ find ((_ == k) <<< fst) qs
_body :: Effect HTMLElement
_body = unsafePartial $ pure <<< fromJust =<< HD.body =<< _doc
_mt :: forall a. Effect (Maybe a) -> MaybeT Effect a
_mt = MaybeT
_mt_pure :: forall a. Maybe a -> MaybeT Effect a
_mt_pure = MaybeT <<< pure
dummyAttr :: forall r i. HP.IProp r i
dummyAttr = HP.attr (HH.AttrName "data-dummy") ""
whenP :: forall r i. Boolean -> HP.IProp r i -> HP.IProp r i
whenP b p = if b then p else dummyAttr
maybeP :: forall a r i. Maybe a -> (a -> HP.IProp r i) -> HP.IProp r i
maybeP m p = maybe dummyAttr p m
whenC :: Boolean -> ClassName -> ClassName
whenC b c = if b then c else ClassName ""
whenH :: forall p i. Boolean -> (Unit -> HH.HTML p i) -> HH.HTML p i
whenH b k = if b then k unit else HH.text ""
maybeH :: forall a p i. Maybe a -> (a -> HH.HTML p i) -> HH.HTML p i
maybeH m k = maybe (HH.text "") k m
fromNullableStr :: Nullable String -> String
fromNullableStr = fromMaybe "" <<< toMaybe

9
purs/test/Main.purs Normal file
View file

@ -0,0 +1,9 @@
module Test.Main where
import Prelude
import Effect (Effect)
import Effect.Console (log)
main :: Effect Unit
main = do
log "You should add some tests."

88
sample-bookmarks.json Normal file
View file

@ -0,0 +1,88 @@
[{"href":"http://math.andrej.com/2012/11/08/how-to-implement-dependent-type-theory-i/","description":"How to implement dependent type theory I | Mathematics and Computation","extended":"","time":"2018-03-02T21:37:18Z","shared":"yes","toread":"no","tags":"dependenttypes"},
{"href":"https://www.newyorker.com/magazine/2015/05/11/overkill-atul-gawande","description":"America\u2019s Epidemic of Unnecessary Care | The New Yorker","extended":"","time":"2018-03-02T19:26:55Z","shared":"yes","toread":"no","tags":"medicie health"},
{"href":"https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis","description":"Functional options for friendly APIs | Dave Cheney","extended":"","time":"2018-03-02T19:24:40Z","shared":"yes","toread":"no","tags":"functionaloptions golang"},
{"href":"https://github.com/swagger-api/swagger-codegen/wiki/Swagger-Codegen-migration-(swagger-codegen-generators-repository)","description":"Swagger Codegen migration (swagger codegen generators repository) \u00b7 swagger-api/swagger-codegen Wiki","extended":"","time":"2018-03-02T16:06:09Z","shared":"yes","toread":"no","tags":"swagger-codegen"},
{"href":"https://news.ycombinator.com/item?id=16493727","description":"LimeSDR Now Backed by the European Space Agency | Hacker News","extended":"","time":"2018-03-02T15:52:26Z","shared":"yes","toread":"no","tags":"sdr"},
{"href":"http://www.tobiastoft.com/posts/an-intro-to-pen-plotters","description":"An intro to Pen Plotters \u2014 Hej.","extended":"","time":"2018-03-02T02:50:35Z","shared":"yes","toread":"no","tags":"penplotter"},
{"href":"https://news.ycombinator.com/item?id=16493489","description":"Machine Learning Crash Course | Hacker News","extended":"","time":"2018-03-01T19:53:53Z","shared":"yes","toread":"no","tags":"machinelearning"},
{"href":"https://developers.google.com/machine-learning/crash-course/","description":"Machine Learning Crash Course \u00a0|\u00a0 Google Developers","extended":"","time":"2018-03-01T19:23:58Z","shared":"yes","toread":"no","tags":"machinelearning"},
{"href":"https://www.reaktor.com/blog/fear-trust-and-javascript/","description":"Fear, trust and JavaScript: When types and functional programming fail - Reaktor","extended":"","time":"2018-03-01T19:05:12Z","shared":"yes","toread":"no","tags":""},
{"href":"http://jxv.io/blog/2018-02-28-A-Game-in-Haskell.html","description":"A Game in Haskell - Dino Rush","extended":"","time":"2018-03-01T13:33:10Z","shared":"yes","toread":"no","tags":"game"},
{"href":"https://arstechnica.com/science/2018/02/signal-of-the-universes-first-stars-hints-at-odd-form-of-dark-matter/","description":"Simple telescope picks up hint of the Universe\u2019s first stars, dark matter | Ars Technica","extended":"","time":"2018-03-01T07:19:56Z","shared":"yes","toread":"no","tags":"darkmatter"},
{"href":"https://blogs.scientificamerican.com/observations/a-potentially-game-changing-message-from-the-dawn-of-time/","description":"A Potentially Game-Changing Message from the Dawn of Time - Scientific American Blog Network","extended":"","time":"2018-03-01T07:19:51Z","shared":"yes","toread":"no","tags":"darkmatter"},
{"href":"https://www.nature.com/articles/d41586-018-02616-8","description":"Astronomers detect light from the Universe\u2019s first stars","extended":"","time":"2018-03-01T07:19:48Z","shared":"yes","toread":"no","tags":"darkmatter"},
{"href":"https://www.nature.com/articles/nature25791","description":"Possible interaction between baryons and dark-matter particles revealed by the first stars | Nature","extended":"","time":"2018-03-01T07:19:44Z","shared":"yes","toread":"no","tags":"darkmatter"},
{"href":"https://www.theverge.com/2018/2/27/17054740/palantir-predictive-policing-tool-new-orleans-nopd","description":"Palantir has secretly been using New Orleans to test its predictive policing technology - The Verge","extended":"","time":"2018-03-01T03:40:47Z","shared":"yes","toread":"no","tags":"police"},
{"href":"https://aaronweiss.us/posts/2018-02-26-reasoning-with-types-in-rust.html","description":"Aaron Weiss / Reasoning with Types in Rust","extended":"","time":"2018-03-01T01:46:51Z","shared":"yes","toread":"no","tags":"rust"},
{"href":"https://www.washingtonpost.com/opinions/hope-hicks-says-she-lies-for-trump-thats-encouraging/2018/02/28/09e61982-1cc3-11e8-9de1-147dd2df3829_story.html","description":"Hope Hicks told the truth about lying for Trump. Now she\u2019s gone. - The Washington Post","extended":"","time":"2018-03-01T01:42:24Z","shared":"yes","toread":"no","tags":"hopehicks"},
{"href":"https://www.0x0ff.info/wp-content/uploads/2014/02/cheat-sheet.png","description":"cheat-sheet.png (3508\u00d72479)","extended":"","time":"2018-02-28T22:06:47Z","shared":"yes","toread":"no","tags":""},
{"href":"http://socialturkers.com/","description":"social turkers","extended":"socialturkers","time":"2018-02-28T21:33:27Z","shared":"yes","toread":"no","tags":"socialturkers"},
{"href":"http://networkcultures.org/moneylab/2018/02/07/the-blockchain-as-a-modulator-of-existence/","description":"MoneyLab | The Blockchain as a Modulator of Existence","extended":"","time":"2018-02-28T21:20:52Z","shared":"yes","toread":"no","tags":"blockchain"},
{"href":"http://neilmitchell.blogspot.com/2008/02/adding-data-files-using-cabal.html","description":"Neil Mitchell's Haskell Blog: Adding data files using Cabal","extended":"","time":"2018-02-28T21:14:53Z","shared":"yes","toread":"no","tags":"haskell"},
{"href":"https://github.com/jkachmar/jkachmar-lambdaconf-cfp-2018/blob/master/commented-cfp.md","description":"jkachmar-lambdaconf-cfp-2018/commented-cfp.md at master \u00b7 jkachmar/jkachmar-lambdaconf-cfp-2018","extended":"","time":"2018-02-28T20:22:04Z","shared":"yes","toread":"no","tags":"lambdaconf"},
{"href":"https://gist.github.com/jkachmar/f1e1544524820ad6eb49524a2327d3a8","description":"Servant EKG 0.12","extended":"","time":"2018-02-28T19:58:46Z","shared":"yes","toread":"no","tags":"jkachmar"},
{"href":"https://gist.github.com/jkachmar/b3baedc4f3eacce3d6cc8cb790447eb7","description":"Esqueleto tests for SqlReadT","extended":"","time":"2018-02-28T19:58:29Z","shared":"yes","toread":"no","tags":"jkachmar"},
{"href":"https://stackoverflow.com/questions/48954495/is-it-possible-to-get-all-contexts-of-a-traversable-lazily","description":"haskell - Is it possible to get all contexts of a Traversable lazily? - Stack Overflow","extended":"","time":"2018-02-28T19:50:16Z","shared":"yes","toread":"no","tags":"haskell"},
{"href":"https://blog.github.com/2017-11-29-use-any-theme-with-github-pages/","description":"Use any theme with GitHub Pages | The GitHub Blog","extended":"","time":"2018-02-28T19:46:33Z","shared":"yes","toread":"no","tags":"theme"},
{"href":"https://mtlynch.io/why-i-quit-google/","description":"Why I Quit Google to Work for Myself - Silly Bits","extended":"","time":"2018-02-28T19:44:38Z","shared":"yes","toread":"no","tags":"google"},
{"href":"http://www.olioapps.com/blog/the-lost-art-of-the-makefile/","description":"The Lost Art of the Makefile","extended":"","time":"2018-02-28T19:22:19Z","shared":"yes","toread":"no","tags":"makefile"},
{"href":"https://www.youtube.com/results?search_query=Politicon","description":"Politicon - YouTube","extended":"","time":"2018-02-28T17:04:12Z","shared":"yes","toread":"no","tags":"Politicon"},
{"href":"https://politicon.com/about-politicon/","description":"\u00bb About Politicon","extended":"","time":"2018-02-28T17:03:48Z","shared":"yes","toread":"no","tags":"Politicon"},
{"href":"https://www.facebook.com/TheYoungTurks/videos/10155481460764205/","description":"The Young Turks - Ben Shapiro, the self proclaimed free speech...","extended":"","time":"2018-02-28T17:00:37Z","shared":"yes","toread":"no","tags":"tyt"},
{"href":"https://please.build/","description":"Please","extended":"","time":"2018-02-28T16:46:30Z","shared":"yes","toread":"no","tags":"please build"},
{"href":"https://camdavidsonpilon.github.io/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers/","description":"Bayesian Methods for Hackers","extended":"","time":"2018-02-28T16:38:26Z","shared":"yes","toread":"no","tags":"bayesian probability python statistics"},
{"href":"https://www.tweag.io/posts/2018-02-28-bazel-haskell.html","description":"Tweag I/O - Build large polyglot projects with Bazel... now with Haskell support","extended":"","time":"2018-02-28T15:13:00Z","shared":"yes","toread":"no","tags":""},
{"href":"http://www.parsonsmatt.org/2016/12/18/servant_in_yesod_-_yo_dawg.html","description":"Servant in Yesod - Yo Dawg","extended":"","time":"2018-02-27T21:21:53Z","shared":"yes","toread":"no","tags":"yesod"},
{"href":"https://ghc.haskell.org/trac/ghc/ticket/9706","description":"#9706 (New block-structured heap organization for 64-bit) \u2013 GHC","extended":"","time":"2018-02-27T17:09:26Z","shared":"yes","toread":"no","tags":"ghc"},
{"href":"https://github.com/Microsoft/WSL/issues/1671","description":"`stack ghc` painfully slow \u00b7 Issue #1671 \u00b7 Microsoft/WSL","extended":"","time":"2018-02-27T17:07:48Z","shared":"yes","toread":"no","tags":"ghc wsl"},
{"href":"https://www.atlasobscura.com/articles/blankets-summer-hot","description":"Why Do We Sleep Under Blankets, Even on the Hottest Nights? - Atlas Obscura","extended":"","time":"2018-02-27T16:43:23Z","shared":"yes","toread":"no","tags":"blankets"},
{"href":"http://www.leshatton.org/Documents/OO_IS698.pdf","description":"","extended":"","time":"2018-02-27T16:31:02Z","shared":"yes","toread":"no","tags":"oo OO_IS698"},
{"href":"http://gameprogrammingpatterns.com/command.html","description":"Command \u00b7 Design Patterns Revisited \u00b7 Game Programming Patterns","extended":"","time":"2018-02-27T16:17:35Z","shared":"yes","toread":"yes","tags":"command"},
{"href":"https://ericlippert.com/2015/05/04/wizards-and-warriors-part-three/","description":"Wizards and warriors, part three | Fabulous adventures in coding","extended":"","time":"2018-02-26T22:58:37Z","shared":"yes","toread":"no","tags":"ericlippert"},
{"href":"https://ericlippert.com/2015/05/07/wizards-and-warriors-part-four/","description":"Wizards and warriors, part four | Fabulous adventures in coding","extended":"","time":"2018-02-26T22:58:36Z","shared":"yes","toread":"no","tags":"ericlippert"},
{"href":"https://ericlippert.com/2015/05/11/wizards-and-warriors-part-five/","description":"Wizards and warriors, part five | Fabulous adventures in coding","extended":"","time":"2018-02-26T22:58:32Z","shared":"yes","toread":"no","tags":"ericlippert"},
{"href":"https://ericlippert.com/2015/04/30/wizards-and-warriors-part-two/","description":"Wizards and warriors, part two | Fabulous adventures in coding","extended":"","time":"2018-02-26T22:58:23Z","shared":"yes","toread":"no","tags":"ericlippert"},
{"href":"https://ericlippert.com/2015/04/27/wizards-and-warriors-part-one/","description":"Wizards and warriors, part one | Fabulous adventures in coding","extended":"","time":"2018-02-26T22:58:16Z","shared":"yes","toread":"no","tags":"ericlippert"},
{"href":"https://news.ycombinator.com/item?id=16468280","description":"How I Learned to Stop Worrying and Love the State Machine | Hacker News","extended":"When I'm stuck on a software design problem, pick some random part of the program and see what happens if I make it first class.","time":"2018-02-26T22:57:38Z","shared":"yes","toread":"no","tags":"raganwald"},
{"href":"http://raganwald.com/2018/02/23/forde.html","description":"Forde's Tenth Rule, or, \"How I Learned to Stop Worrying and \u2764\ufe0f the State Machine\"","extended":"","time":"2018-02-26T22:57:20Z","shared":"yes","toread":"yes","tags":"raganwald"},
{"href":"http://downloads.haskell.org/~ghc/latest/docs/html/users_guide/flags.html","description":"7.6. Flag reference \u2014 Glasgow Haskell Compiler 8.2.2 User's Guide","extended":"-fprint-expanded-synonyms","time":"2018-02-26T21:52:02Z","shared":"yes","toread":"no","tags":""},
{"href":"https://www.google.com/search?q=codetermination&oq=codetermination&aqs=chrome..69i57.1655j0j7&sourceid=chrome&ie=UTF-8","description":"codetermination - Google Search","extended":"","time":"2018-02-26T20:38:10Z","shared":"yes","toread":"no","tags":"codetermination"},
{"href":"https://news.ycombinator.com/item?id=16463069","description":"Why can\u2019t women get pregnant without the menstrual cycle? (2016) | Hacker News","extended":"","time":"2018-02-26T13:24:34Z","shared":"yes","toread":"no","tags":""},
{"href":"http://beej.us/guide/bgnet/html/multi/index.html","description":"Beej's Guide to Network Programming","extended":"","time":"2018-02-25T21:25:03Z","shared":"yes","toread":"no","tags":"beej network"},
{"href":"https://github.com/mqtt/mqtt.github.io/wiki/Basic-Concepts","description":"Basic Concepts \u00b7 mqtt/mqtt.github.io Wiki","extended":"","time":"2018-02-25T21:23:13Z","shared":"yes","toread":"no","tags":"mqtt"},
{"href":"http://mqtt.org/","description":"MQTT","extended":"","time":"2018-02-25T21:22:42Z","shared":"yes","toread":"no","tags":"mqtt"},
{"href":"https://www.thethingsnetwork.org/docs/network/cli/quick-start.html","description":"Quick Start | The Things Network","extended":"","time":"2018-02-25T21:20:22Z","shared":"yes","toread":"no","tags":"ttn"},
{"href":"https://www.thethingsnetwork.org/forum/t/chicago-illinois/351","description":"Chicago, Illinois - Communities / Find people from your city or area - The Things Network","extended":"","time":"2018-02-25T21:19:15Z","shared":"yes","toread":"no","tags":"chicago"},
{"href":"https://www.thethingsnetwork.org/community/chicago/","description":"Chicago - The Things Network Community","extended":"","time":"2018-02-25T21:14:24Z","shared":"yes","toread":"no","tags":"lora"},
{"href":"https://jozefg.bitbucket.io/posts/2015-01-08-modules.html","description":"C&C - A Crash Course on ML Modules","extended":"","time":"2018-02-25T19:50:12Z","shared":"yes","toread":"no","tags":"ml modules"},
{"href":"https://defn.io/2018/02/25/web-app-from-scratch-01/","description":"Web application from scratch, Part I \u00b7 Bogdan Popa","extended":"","time":"2018-02-25T19:41:47Z","shared":"yes","toread":"no","tags":"pythom"},
{"href":"https://news.ycombinator.com/item?id=16456792","description":"Overconfident Students, Dubious Employers | Hacker News","extended":"I'm always a little skeptical of these sorts of surveys because it's hard to tease out what people believe about themselves because it's true vs. what people believe about themselves because it's useful.\r\nI remember that when I was a new grad, there was a very large part of myself that held a realistic appraisal of my abilities and was therefore scared shitless about my ability to make it in the working world. I was very careful to never let that part of me out in interviews - or, for that matter, to anyone. Confidence only works if you keep up the illusion so thoroughly that it ceases to be an illusion.\r\n\r\nAnd it worked. I got a job at a financial software startup, and then was put in charge of projects that no new grad should ever have been put in charge of. I grew into the role. I left to go found a startup, which is also something that someone with 2 years of work experience had no business doing. That worked too - I may not have been qualified to found a startup, but when I folded it up, I was a lot more qualified as an engineer than most of my other peers with 4 years of work experience. So Google hired me to work on the front page of the search engine, and I grew into that role too.\r\n\r\nThe majority of my classmates let their accurate perceptions of what they were actually qualified to do govern what they applied to do, and as a result, many were still struggling to get into a career 10 years later. By that point, your self-perception has become reality, and it's much harder to convince potential employers to take the risk that you'll grow into the position. Then they wake up and realize that everybody's faking it and their new manager isn't actually all that much more skilled than them, but (barring a career reset like going to grad school) it's difficult to reset people's perceptions.","time":"2018-02-25T13:55:18Z","shared":"yes","toread":"no","tags":"hn"},
{"href":"https://comminos.com/css/default.css","description":"","extended":"","time":"2018-02-25T04:25:50Z","shared":"yes","toread":"no","tags":"css"},
{"href":"https://www.yesodweb.com/blog/2012/08/classy-prelude-good-bad-ugly","description":"ClassyPrelude: The good, the bad, and the ugly","extended":"","time":"2018-02-24T21:32:59Z","shared":"yes","toread":"no","tags":"classyprelude"},
{"href":"https://medium.com/incerto/the-most-intolerant-wins-the-dictatorship-of-the-small-minority-3f1f83ce4e15","description":"The Most Intolerant Wins: The Dictatorship of the Small Minority","extended":"","time":"2018-02-24T19:24:12Z","shared":"yes","toread":"no","tags":"taleb"},
{"href":"https://www.quora.com/Define-INFRINGED-as-it-is-used-in-the-second-amendment-What-is-unclear-about-this-If-lawmakers-want-to-infringe-upon-our-right-to-bear-arms-why-dont-they-follow-the-law-and-amend-the-Constitution","description":"Define 'INFRINGED' as it is used in the second amendment. What is unclear about this? If lawmakers want to infringe upon our right to bear arms, why don't they follow the law and amend the Constitution? - Quora","extended":"","time":"2018-02-24T18:09:32Z","shared":"yes","toread":"no","tags":"infringed"},
{"href":"https://mitpress.mit.edu/books/functional-differential-geometry","description":"Functional Differential Geometry | The MIT Press","extended":"","time":"2018-02-24T18:02:29Z","shared":"yes","toread":"no","tags":"physics"},
{"href":"https://mitpress.mit.edu/sites/default/files/titles/content/sicm_edition_2/book.html","description":"Structure and Interpretation of Classical Mechanics","extended":"","time":"2018-02-24T17:59:27Z","shared":"yes","toread":"no","tags":"sicp"},
{"href":"https://www.nytimes.com/2018/02/23/opinion/brian-mast-assault-weapons-ban.html?mtrref=www.facebook.com&gwh=B320C7668EBB9993E9F5B4BC712DB4D7&gwt=pay&assetType=opinion","description":"","extended":"","time":"2018-02-24T16:57:13Z","shared":"yes","toread":"no","tags":""},
{"href":"http://sqlite.org/lang_createtable.html#uniqueconst","description":"SQLite Query Language: CREATE TABLE","extended":"","time":"2018-02-24T04:26:51Z","shared":"yes","toread":"no","tags":"sqlite unique"},
{"href":"http://cockpit-project.org/","description":"Cockpit Project \u2014 Cockpit Project","extended":"","time":"2018-02-24T00:04:00Z","shared":"yes","toread":"no","tags":""},
{"href":"http://baatz.io/posts/haskell-in-a-startup/","description":"A founder's perspective on 4 years with Haskell","extended":"","time":"2018-02-23T20:38:27Z","shared":"yes","toread":"no","tags":"haskell"},
{"href":"https://github.com/trending/haskell","description":"Trending Haskell repositories on GitHub today","extended":"","time":"2018-02-23T20:35:00Z","shared":"yes","toread":"no","tags":"trending github haskell"},
{"href":"https://newrepublic.com/article/147111/zadie-smiths-book-essays-explores-means-human","description":"Zadie Smith\u2019s Book of Essays Explores What It Means to Be Human | New Republic","extended":"","time":"2018-02-23T20:33:55Z","shared":"yes","toread":"yes","tags":"zadiesmith lit"},
{"href":"https://nymag.com/daily/intelligencer/2018/02/americas-opioid-epidemic.html","description":"Andrew Sullivan on the Opioid Epidemic in America","extended":"","time":"2018-02-23T19:11:10Z","shared":"yes","toread":"no","tags":"editorial"},
{"href":"https://news.ycombinator.com/item?id=16445950","description":"The Poison We Pick | Hacker News","extended":"","time":"2018-02-23T19:11:02Z","shared":"yes","toread":"yes","tags":"editorial"},
{"href":"https://ftalphaville.ft.com/2018/02/15/2198809/someone-is-wrong-on-the-internet-millennial-savings-edition/","description":"Someone is wrong on the internet, millennial savings edition | FT Alphaville","extended":"","time":"2018-02-23T19:10:15Z","shared":"yes","toread":"no","tags":"housing"},
{"href":"https://speakerdeck.com/justinwoo/easy-json-deserialization-with-simple-json-and-record","description":"Easy JSON deserialization with Simple-JSON and Record // Speaker Deck","extended":"","time":"2018-02-23T07:19:52Z","shared":"yes","toread":"no","tags":"haskell"},
{"href":"http://www.ikea.com/us/en/catalog/products/00103102/","description":"MARKUS Swivel chair - Glose black - IKEA","extended":"","time":"2018-02-23T05:23:13Z","shared":"yes","toread":"no","tags":"Ikea Markus"},
{"href":"https://kono.store/products/kira-mechanical-keyboard","description":"Kira Mechanical Keyboard \u2013 Kono Store","extended":"","time":"2018-02-23T05:19:26Z","shared":"yes","toread":"no","tags":"kira"},
{"href":"https://www.reddit.com/r/MechanicalKeyboards/comments/7zhhje/battlestation/","description":"Battlestation : MechanicalKeyboards","extended":"","time":"2018-02-23T05:15:59Z","shared":"yes","toread":"no","tags":"keyboard"},
{"href":"https://soundcloud.com/jordanpetersonpodcast","description":"The Jordan B Peterson Podcast | Free Listening on SoundCloud","extended":"","time":"2018-02-23T05:13:37Z","shared":"yes","toread":"no","tags":"jordanpetersonpodcast"},
{"href":"https://www.theatlantic.com/politics/archive/2018/02/what-i-saw-treating-the-victims-from-parkland-should-change-the-debate-on-guns/553937/","description":"The AR-15 Is Different: What I Learned Treating Parkland Victims - The Atlantic","extended":"","time":"2018-02-23T05:00:55Z","shared":"yes","toread":"no","tags":"ar15"},
{"href":"https://jacobian.org/writing/python-environment-2018/","description":"My Python Development Environment, 2018 Edition \u00ab Jacob Kaplan-Moss","extended":"","time":"2018-02-22T22:50:41Z","shared":"yes","toread":"no","tags":"python"},
{"href":"https://www.youtube.com/watch?v=F4VZPxLZUdA&amp=&t=1625s","description":"Building test check Generators - Gary Fredericks - YouTube","extended":"","time":"2018-02-22T22:45:33Z","shared":"yes","toread":"no","tags":"propertybasedtesting"},
{"href":"https://keybase.io/docs/the_app/linux_expired_key","description":"Keybase","extended":"","time":"2018-02-22T22:23:28Z","shared":"yes","toread":"no","tags":"keybase"},
{"href":"https://underscore.io/blog/posts/2017/06/02/uniting-church-and-state.html","description":"Uniting Church and State: FP and OO Together - Underscore","extended":"","time":"2018-02-22T20:57:05Z","shared":"yes","toread":"no","tags":"fpoo"},
{"href":"https://www.cnn.com/2018/02/22/politics/cnn-town-hall-full-video-transcript/index.html","description":"CNN town hall: Students question lawmakers, NRA (full transcript, video) - CNNPolitics","extended":"","time":"2018-02-22T20:52:16Z","shared":"yes","toread":"no","tags":"cnn town hall sunrise"},
{"href":"http://yourbasic.org/golang/your-basic-func/","description":"Your basic func | yourbasic.org","extended":"","time":"2018-02-22T20:19:55Z","shared":"yes","toread":"no","tags":"graph golang"},
{"href":"https://operand.ca/2018/02/22/liberating-a-x200.html","description":"","extended":"","time":"2018-02-22T20:16:34Z","shared":"yes","toread":"no","tags":"thinkpad"},
{"href":"https://arxiv.org/abs/1802.07228","description":"[1802.07228] The Malicious Use of Artificial Intelligence: Forecasting, Prevention, and Mitigation","extended":"","time":"2018-02-22T20:10:25Z","shared":"yes","toread":"no","tags":"malevolent"}]

3
sample-migrate.sh Executable file
View file

@ -0,0 +1,3 @@
stack exec migration -- createdb --conn espial.sqlite3
stack exec migration -- createuser --conn espial.sqlite3 --userName myusername --userPassword myuserpassword
stack exec migration -- importbookmarks --conn espial.sqlite3 --userName myusername --bookmarkFile sample-bookmarks.json

182
src/Application.hs Normal file
View file

@ -0,0 +1,182 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Application
( getApplicationDev
, appMain
, develMain
, makeFoundation
, makeLogWare
-- * for DevelMain
, getApplicationRepl
, shutdownApp
-- * for GHCI
, handler
, db
) where
import Control.Monad.Logger (liftLoc, runLoggingT)
import Database.Persist.Sqlite
(createSqlitePool, sqlDatabase, sqlPoolSize)
import Import
import Yesod.Auth (getAuth)
import Language.Haskell.TH.Syntax (qLocation)
import Lens.Micro
import Network.HTTP.Client.TLS
import Network.Wai (Middleware)
import Network.Wai.Middleware.Autohead
import Network.Wai.Middleware.AcceptOverride
import Network.Wai.Middleware.Gzip
import Network.Wai.Middleware.MethodOverride
import Network.Wai.Handler.Warp
(Settings, defaultSettings, defaultShouldDisplayException,
runSettings, setHost, setOnException, setPort, getPort)
import Network.Wai.Middleware.RequestLogger
(Destination(Logger), IPAddrSource(..), OutputFormat(..),
destination, mkRequestLogger, outputFormat)
import System.Log.FastLogger
(defaultBufSize, newStdoutLoggerSet, toLogStr)
import qualified Control.Monad.Metrics as MM
import qualified Network.Wai.Metrics as WM
import qualified System.Metrics as EKG
import qualified System.Remote.Monitoring as EKG
-- Import all relevant handler modules here.
-- Don't forget to add new modules to your cabal file!
import Handler.Common
import Handler.Home
import Handler.User
import Handler.AccountSettings
import Handler.Add
import Handler.Edit
import Handler.Notes
import Handler.Docs
mkYesodDispatch "App" resourcesApp
makeFoundation :: AppSettings -> IO App
makeFoundation appSettings = do
appHttpManager <- getGlobalManager
appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger
store <- EKG.newStore
EKG.registerGcMetrics store
appMetrics <- MM.initializeWith store
appStatic <-
(if appMutableStatic appSettings
then staticDevel
else static)
(appStaticDir appSettings)
let mkFoundation appConnPool = App { ..}
tempFoundation = mkFoundation (error "connPool forced in tempFoundation")
logFunc = messageLoggerSource tempFoundation appLogger
pool <-
flip runLoggingT logFunc $
createSqlitePool
(sqlDatabase (appDatabaseConf appSettings))
(sqlPoolSize (appDatabaseConf appSettings))
-- runLoggingT
-- (runSqlPool runMigrations pool)
-- logFunc
return (mkFoundation pool)
makeApplication :: App -> IO Application
makeApplication foundation = do
logWare <- makeLogWare foundation
appPlain <- toWaiAppPlain foundation
let store = appMetrics foundation ^. MM.metricsStore
waiMetrics <- WM.registerWaiMetrics store
return (logWare (makeMiddleware waiMetrics appPlain))
makeMiddleware :: WM.WaiMetrics -> Middleware
makeMiddleware waiMetrics =
WM.metrics waiMetrics .
acceptOverride .
autohead .
gzip def {gzipFiles = GzipPreCompressed GzipIgnore} .
methodOverride
makeLogWare :: App -> IO Middleware
makeLogWare foundation =
mkRequestLogger
def
{ outputFormat =
if appDetailedRequestLogging (appSettings foundation)
then Detailed True
else Apache
(if appIpFromHeader (appSettings foundation)
then FromFallback
else FromSocket)
, destination = Logger (loggerSet (appLogger foundation))
}
-- | Warp settings for the given foundation value.
warpSettings :: App -> Settings
warpSettings foundation =
setPort (appPort (appSettings foundation)) $
setHost (appHost (appSettings foundation)) $
setOnException
(\_req e ->
when (defaultShouldDisplayException e) $
messageLoggerSource
foundation
(appLogger foundation)
$(qLocation >>= liftLoc)
"yesod"
LevelError
(toLogStr $ "Exception from Warp: " ++ show e))
defaultSettings
-- | For yesod devel, return the Warp settings and WAI Application.
getApplicationDev :: IO (Settings, Application)
getApplicationDev = do
settings <- getAppSettings
foundation <- makeFoundation settings
wsettings <- getDevSettings (warpSettings foundation)
app <- makeApplication foundation
forkEKG foundation
return (wsettings, app)
getAppSettings :: IO AppSettings
getAppSettings = loadYamlSettings [configSettingsYml] [] useEnv
-- | main function for use by yesod devel
develMain :: IO ()
develMain = develMainHelper getApplicationDev
forkEKG :: App -> IO ()
forkEKG foundation =
let settings = appSettings foundation in
for_ (appEkgHost settings) $ \ekgHost ->
for_ (appEkgPort settings) $ \ekgPort ->
EKG.forkServerWith
(appMetrics foundation ^. MM.metricsStore)
(encodeUtf8 ekgHost)
ekgPort
-- | The @main@ function for an executable running this site.
appMain :: IO ()
appMain = do
settings <- loadYamlSettingsArgs [configSettingsYmlValue] useEnv
foundation <- makeFoundation settings
app <- makeApplication foundation
forkEKG foundation
runSettings (warpSettings foundation) app
getApplicationRepl :: IO (Int, App, Application)
getApplicationRepl = do
settings <- getAppSettings
foundation <- makeFoundation settings
wsettings <- getDevSettings (warpSettings foundation)
app1 <- makeApplication foundation
return (getPort wsettings, foundation, app1)
shutdownApp :: App -> IO ()
shutdownApp _ = return ()
-- | Run a handler
handler :: Handler a -> IO a
handler h = getAppSettings >>= makeFoundation >>= flip unsafeHandler h
-- | Run DB queries
db :: ReaderT SqlBackend (HandlerFor App) a -> IO a
db = handler . runDB

250
src/Foundation.hs Normal file
View file

@ -0,0 +1,250 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
module Foundation where
import Import.NoFoundation
import Database.Persist.Sql (ConnectionPool, runSqlPool)
import Text.Hamlet (hamletFile)
import Text.Jasmine (minifym)
import PathPiece()
-- import Yesod.Auth.Dummy
import Yesod.Default.Util (addStaticContentExternal)
import Yesod.Core.Types
import Yesod.Auth.Message
import qualified Network.Wai as NW
import qualified Control.Monad.Metrics as MM
import qualified Data.CaseInsensitive as CI
import qualified Data.Text.Encoding as TE
import qualified Yesod.Core.Unsafe as Unsafe
data App = App
{ appSettings :: AppSettings
, appStatic :: Static -- ^ Settings for static file serving.
, appConnPool :: ConnectionPool -- ^ Database connection pool.
, appHttpManager :: Manager
, appLogger :: Logger
, appMetrics :: !MM.Metrics
} deriving (Typeable)
mkYesodData "App" $(parseRoutesFile "config/routes")
deriving instance Typeable Route
deriving instance Generic (Route App)
-- YesodPersist
instance YesodPersist App where
type YesodPersistBackend App = SqlBackend
runDB action = do
master <- getYesod
runSqlPool action (appConnPool master)
instance YesodPersistRunner App where
getDBRunner = defaultGetDBRunner appConnPool
-- Yesod
instance Yesod App where
approot = ApprootRequest $ \app req ->
case appRoot (appSettings app) of
Nothing -> getApprootText guessApproot app req
Just root -> root
makeSessionBackend _ = Just <$> defaultClientSessionBackend
10080 -- min (7 days)
"config/client_session_key.aes"
yesodMiddleware = metricsMiddleware . defaultYesodMiddleware . defaultCsrfMiddleware
defaultLayout widget = do
req <- getRequest
master <- getYesod
urlrender <- getUrlRender
mmsg <- getMessage
musername <- maybeAuthUsername
muser <- (fmap.fmap) snd maybeAuthPair
mcurrentRoute <- getCurrentRoute
void $ mapM (incrementRouteEKG req) mcurrentRoute
pc <- widgetToPageContent $ do
setTitle "Espial"
addAppScripts
addStylesheet (StaticR css_tachyons_min_css)
addStylesheet (StaticR css_main_css)
$(widgetFile "default-layout")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
addStaticContent ext mime content = do
master <- getYesod
let staticDir = appStaticDir (appSettings master)
addStaticContentExternal
minifym
genFileName
staticDir
(StaticR . flip StaticRoute [])
ext
mime
content
where
genFileName lbs = "autogen-" ++ base64md5 lbs
shouldLogIO app _source level =
pure $ appShouldLogAll (appSettings app) || level == LevelWarn || level == LevelError
makeLogger = return . appLogger
authRoute _ = Just (AuthR LoginR)
isAuthorized (AuthR _) _ = pure Authorized
isAuthorized _ _ = pure Authorized
defaultMessageWidget title body = do
setTitle title
toWidget [hamlet|
<main .pv2.ph3.mh1>
<div .w-100.mw8.center>
<div .pa3.bg-near-white>
<h1>#{title}
^{body}
|]
isAuthenticated :: Handler AuthResult
isAuthenticated = maybeAuthId >>= \case
Just authId -> pure Authorized
_ -> pure $ AuthenticationRequired
addAppScripts :: (MonadWidget m, HandlerSite m ~ App) => m ()
addAppScripts = do
addScript (StaticR js_moment_min_js)
addScript (StaticR js_app_min_js)
-- popupLayout
popupLayout :: Widget -> Handler Html
popupLayout widget = do
req <- getRequest
master <- getYesod
mmsg <- getMessage
musername <- maybeAuthUsername
pc <- widgetToPageContent $ do
addAppScripts
addStylesheet (StaticR css_tachyons_min_css)
addStylesheet (StaticR css_popup_css)
$(widgetFile "popup-layout")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
metricsMiddleware :: Handler a -> Handler a
metricsMiddleware handler = do
req <- getRequest
mcurrentRoute <- getCurrentRoute
void $ mapM (incrementRouteEKG req) mcurrentRoute
handler
incrementRouteEKG :: YesodRequest -> Route App -> Handler ()
incrementRouteEKG req = MM.increment . (\r -> "route." <> r <> "." <> method) . pack . constrName
where method = decodeUtf8 $ NW.requestMethod $ reqWaiRequest req
-- YesodAuth
instance YesodAuth App where
type AuthId App = UserId
-- authHttpManager = getHttpManager
authPlugins _ = [dbAuthPlugin]
authenticate = authenticateCreds
loginDest = const HomeR
logoutDest = const HomeR
onLogin = maybeAuth >>= \case
Nothing -> cpprint ("onLogin: could not find user" :: Text)
Just (Entity _ uname) -> setSession userNameKey (userName uname)
onLogout =
deleteSession userNameKey
redirectToReferer = const True
instance YesodAuthPersist App
instance MM.MonadMetrics Handler where
getMetrics = pure . appMetrics =<< getYesod
-- session keys
maybeAuthUsername :: Handler (Maybe Text)
maybeAuthUsername = do
lookupSession userNameKey
ultDestKey :: Text
ultDestKey = "_ULT"
userNameKey :: Text
userNameKey = "_UNAME"
-- dbAuthPlugin
dbAuthPluginName :: Text
dbAuthPluginName = "db"
dbAuthPlugin :: AuthPlugin App
dbAuthPlugin = AuthPlugin dbAuthPluginName dbDispatch dbLoginHandler
where
dbDispatch "POST" ["login"] = dbPostLoginR >>= sendResponse
dbDispatch _ _ = notFound
dbLoginHandler toParent = do
req <- getRequest
lookupSession ultDestKey >>= \case
Just dest | "logout" `isInfixOf` dest -> deleteSession ultDestKey
_ -> pure ()
setTitle "Espial | Log In"
$(widgetFile "login")
dbLoginR :: AuthRoute
dbLoginR = PluginR dbAuthPluginName ["login"]
dbPostLoginR :: AuthHandler master TypedContent
dbPostLoginR = do
mresult <- runInputPostResult (dbLoginCreds
<$> ireq textField "username"
<*> ireq textField "password")
case mresult of
FormSuccess creds -> setCredsRedirect creds
_ -> loginErrorMessageI LoginR InvalidUsernamePass
dbLoginCreds :: Text -> Text -> Creds master
dbLoginCreds username password =
Creds
{ credsPlugin = dbAuthPluginName
, credsIdent = username
, credsExtra = [("password", password)]
}
authenticateCreds ::
(MonadHandler m, HandlerSite m ~ App)
=> Creds App
-> m (AuthenticationResult App)
authenticateCreds Creds {..} = do
muser <-
case credsPlugin of
p | p == dbAuthPluginName -> liftHandler $ runDB $
join <$> mapM (authenticatePassword credsIdent) (lookup "password" credsExtra)
_ -> pure Nothing
case muser of
Nothing -> pure (UserError InvalidUsernamePass)
Just (Entity uid _) -> pure (Authenticated uid)
-- Util
instance RenderMessage App FormMessage where
renderMessage :: App -> [Lang] -> FormMessage -> Text
renderMessage _ _ = defaultFormMessage
instance HasHttpManager App where
getHttpManager :: App -> Manager
getHttpManager = appHttpManager
unsafeHandler :: App -> Handler a -> IO a
unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger

20
src/Generic.hs Normal file
View file

@ -0,0 +1,20 @@
module Generic where
import GHC.Generics
import ClassyPrelude.Yesod
constrName :: (HasConstructor (Rep a), Generic a)=> a -> String
constrName = genericConstrName . from
class HasConstructor (f :: * -> *) where
genericConstrName :: f x -> String
instance HasConstructor f => HasConstructor (D1 c f) where
genericConstrName (M1 x) = genericConstrName x
instance (HasConstructor x, HasConstructor y) => HasConstructor (x :+: y) where
genericConstrName (L1 l) = genericConstrName l
genericConstrName (R1 r) = genericConstrName r
instance Constructor c => HasConstructor (C1 c f) where
genericConstrName x = conName x

View file

@ -0,0 +1,50 @@
module Handler.AccountSettings where
import Import
import qualified ClassyPrelude.Yesod as CP
getAccountSettingsR :: Handler Html
getAccountSettingsR = do
(_, user) <- requireAuthPair
let accountSettingsEl = "accountSettings" :: Text
let accountSettings = toAccountSettingsForm user
defaultLayout $ do
$(widgetFile "user-settings")
toWidgetBody [julius|
app.userR = "@{UserR (UserNameP $ userName user)}";
app.dat.accountSettings = #{ toJSON accountSettings } || [];
|]
toWidget [julius|
PS['Main'].renderAccountSettings('##{rawJS accountSettingsEl}')(app.dat.accountSettings)();
|]
postEditAccountSettingsR :: Handler ()
postEditAccountSettingsR = do
userId <- requireAuthId
accountSettingsForm <- requireCheckJsonBody
runDB (updateUserFromAccountSettingsForm userId accountSettingsForm)
getChangePasswordR :: Handler Html
getChangePasswordR = do
void requireAuthId
req <- getRequest
defaultLayout $
$(widgetFile "change-password")
postChangePasswordR :: Handler Html
postChangePasswordR = do
userId <- requireAuthId
mauthuname <- maybeAuthUsername
mresult <- runInputPostResult ((,) <$> ireq textField "oldpassword" <*> ireq textField "newpassword")
case (mauthuname, mresult) of
(Just uname, FormSuccess (old, new)) -> do
muser <- runDB (authenticatePassword uname old)
case muser of
Just _ -> do
new' <- liftIO (hashPassword new)
void $ runDB (update userId [UserPasswordHash CP.=. new'])
setMessage "Password Changed Successfully"
_ -> setMessage "Incorrect Old Password"
_ -> setMessage "Missing Required Fields"
redirect ChangePasswordR

67
src/Handler/Add.hs Normal file
View file

@ -0,0 +1,67 @@
module Handler.Add where
import Import
import Handler.Archive
import Data.List (nub)
-- View
getAddViewR :: Handler Html
getAddViewR = do
userId <- requireAuthId
murl <- lookupGetParam "url"
mformdb <- runDB (pure . fmap _toBookmarkForm =<< fetchBookmarkByUrl userId murl)
formurl <- bookmarkFormUrl
let renderEl = "addForm" :: Text
popupLayout $ do
toWidget [whamlet|
<div id="#{ renderEl }">
|]
toWidgetBody [julius|
app.dat.bmark = #{ toJSON (fromMaybe formurl mformdb) };
|]
toWidget [julius|
PS['Main'].renderAddForm('##{rawJS renderEl}')(app.dat.bmark)();
|]
bookmarkFormUrl :: Handler BookmarkForm
bookmarkFormUrl = do
Entity _ user <- requireAuth
BookmarkForm
<$> (lookupGetParam "url" >>= pure . fromMaybe "")
<*> (lookupGetParam "title")
<*> (lookupGetParam "description" >>= pure . fmap Textarea)
<*> (lookupGetParam "tags")
<*> (lookupGetParam "private" >>= pure . fmap parseChk <&> (<|> Just (userPrivateDefault user)))
<*> (lookupGetParam "toread" >>= pure . fmap parseChk)
<*> pure Nothing
<*> pure Nothing
<*> pure Nothing
<*> pure Nothing
<*> pure Nothing
where
parseChk s = s == "yes" || s == "on"
-- API
postAddR :: Handler ()
postAddR = do
bookmarkForm <- requireCheckJsonBody
_handleFormSuccess bookmarkForm >>= \case
(Created, bid) -> sendStatusJSON created201 bid
(Updated, _) -> sendResponseStatus noContent204 ()
_handleFormSuccess :: BookmarkForm -> Handler (UpsertResult, Key Bookmark)
_handleFormSuccess bookmarkForm = do
(userId, user) <- requireAuthPair
bm <- liftIO $ _toBookmark userId bookmarkForm
(res, kbid) <- runDB (upsertBookmark mkbid bm tags)
whenM (shouldArchiveBookmark user kbid) $
void $ async (archiveBookmarkUrl kbid (unpack (bookmarkHref bm)))
pure (res, kbid)
where
mkbid = BookmarkKey <$> _bid bookmarkForm
tags = maybe [] (nub . words) (_tags bookmarkForm)

106
src/Handler/Archive.hs Normal file
View file

@ -0,0 +1,106 @@
module Handler.Archive where
import Import
import Data.Function ((&))
import qualified Data.Attoparsec.ByteString.Char8 as AP
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Char8 as BS8
import qualified Network.HTTP.Client as NH
import qualified Network.HTTP.Client.TLS as NH
import qualified Network.HTTP.Types.Status as NH
import qualified Web.FormUrlEncoded as WH
import qualified Control.Monad.Metrics as MM
shouldArchiveBookmark :: User -> Key Bookmark -> Handler Bool
shouldArchiveBookmark user kbid = do
runDB (get kbid) >>= \case
Nothing -> pure False
Just bm -> do
pure $
(isNothing $ bookmarkArchiveHref bm) &&
(bookmarkShared bm)
&& not (_isArchiveBlacklisted bm)
&& not (userPrivacyLock user)
&& userArchiveDefault user
archiveBookmarkUrl :: Key Bookmark -> String -> Handler ()
archiveBookmarkUrl kbid url =
(_fetchArchiveSubmitInfo >>= \case
Left e -> do
MM.increment "archive.fetchSubmitId_noparse"
$(logError) (pack e)
Right submitInfo -> do
userId <- requireAuthId
let req = _buildArchiveSubmitRequest submitInfo url
MM.increment "archive.submit"
res <- liftIO $ NH.httpLbs req =<< NH.getGlobalManager
let status = NH.responseStatus res
MM.increment ("archive.submit_status_" <> (pack.show) (NH.statusCode status))
let updateArchiveUrl = runDB . updateBookmarkArchiveUrl userId kbid . Just
headers = NH.responseHeaders res
case status of
s | s == NH.status200 ->
for_ (lookup "Refresh" headers >>= _parseRefreshHeaderUrl) updateArchiveUrl
s | s == NH.status302 ->
for_ (lookup "Location" headers) (updateArchiveUrl . decodeUtf8)
_ -> $(logError) (pack (show res)))
`catch` (\(e::SomeException) -> ($(logError) $ (pack.show) e) >> throwIO e)
_isArchiveBlacklisted :: Bookmark -> Bool
_isArchiveBlacklisted (Bookmark {..}) =
[ "hulu"
, "livestream"
, "netflix"
, "skillsmatter"
, "twitch.tv"
, "vimeo"
, "youtu.be"
, "youtube"
, "archive."
] &
any (`isInfixOf` bookmarkHref)
_parseRefreshHeaderUrl :: ByteString -> Maybe Text
_parseRefreshHeaderUrl h = do
let u = BS8.drop 1 $ BS8.dropWhile (/= '=') h
if (not (null u))
then Just $ decodeUtf8 u
else Nothing
_buildArchiveSubmitRequest :: (String, String) -> String -> NH.Request
_buildArchiveSubmitRequest (action, submitId) href =
NH.parseRequest_ ("POST " <> action) & \r ->
r { NH.requestHeaders =
[ ("User-Agent", _archiveUserAgent)
, ("Content-Type", "application/x-www-form-urlencoded")
]
, NH.requestBody = NH.RequestBodyLBS $ WH.urlEncodeAsForm ((
[ ("submitid" , submitId)
, ("url", href)
]) :: [(String, String)])
, NH.redirectCount = 0
}
_fetchArchiveSubmitInfo :: Handler (Either String (String , String))
_fetchArchiveSubmitInfo = do
MM.increment "archive.fetchSubmitId"
res <- liftIO $ NH.httpLbs buildSubmitRequest =<< NH.getGlobalManager
MM.increment ("archive.fetchSubmitId_status_" <> (pack.show) (NH.statusCode (NH.responseStatus res)))
let body = LBS.toStrict (responseBody res)
action = _parseSubstring (AP.string "action=\"") (AP.notChar '"') body
submitId = _parseSubstring (AP.string "submitid\" value=\"") (AP.notChar '"') body
pure $ (,) <$> action <*> submitId
where
buildSubmitRequest =
NH.parseRequest_ "https://archive.li/" & \r ->
r {NH.requestHeaders = [("User-Agent", _archiveUserAgent)]}
_archiveUserAgent :: ByteString
_archiveUserAgent = "espial"
_parseSubstring :: AP.Parser ByteString -> AP.Parser Char -> BS.ByteString -> Either String String
_parseSubstring start inner res = do
(flip AP.parseOnly) res (skipAnyTill start >> AP.many1 inner)
where
skipAnyTill end = go where go = end *> pure () <|> AP.anyChar *> go

31
src/Handler/Common.hs Normal file
View file

@ -0,0 +1,31 @@
-- | Common handler functions.
module Handler.Common where
import Import
import Data.FileEmbed (embedFile)
import Text.Read
-- These handlers embed files in the executable at compile time to avoid a
-- runtime dependency, and for efficiency.
getFaviconR :: Handler TypedContent
getFaviconR = do cacheSeconds $ 60 * 5
--cacheSeconds $ 60 * 60 * 24 * 30 -- cache for a month
return $ TypedContent "image/x-icon"
$ toContent $(embedFile "config/favicon.ico")
getRobotsR :: Handler TypedContent
getRobotsR = return $ TypedContent typePlain
$ toContent $(embedFile "config/robots.txt")
lookupPagingParams :: Handler (Maybe Int64, Maybe Int64)
lookupPagingParams = do
cq <- fmap parseMaybe (lookupGetParam "count")
cs <- fmap parseMaybe (lookupSession "count")
for_ cq (setSession "count" . (pack . show))
pq <- fmap parseMaybe (lookupGetParam "page")
pure (cq <|> cs, pq)
where
parseMaybe x = readMaybe . unpack =<< x

9
src/Handler/Docs.hs Normal file
View file

@ -0,0 +1,9 @@
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
module Handler.Docs where
import Import
getDocsSearchR :: Handler Html
getDocsSearchR = popupLayout $
$(widgetFile "docs-search")

51
src/Handler/Edit.hs Normal file
View file

@ -0,0 +1,51 @@
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
module Handler.Edit where
import Database.Persist.Sql
import Import
-- routes
deleteDeleteR :: Int64 -> Handler Html
deleteDeleteR bid = do
userId <- requireAuthId
runDB $ do
let k_bid = BookmarkKey bid
_ <- requireResource userId k_bid
deleteCascade k_bid
return ""
postReadR :: Int64 -> Handler Html
postReadR bid = do
userId <- requireAuthId
runDB $ do
let k_bid = BookmarkKey bid
_ <- requireResource userId k_bid
update k_bid [BookmarkToRead =. False]
return ""
postStarR :: Int64 -> Handler Html
postStarR bid = _setSelected bid True
postUnstarR :: Int64 -> Handler Html
postUnstarR bid = _setSelected bid False
-- common
_setSelected :: Int64 -> Bool -> Handler Html
_setSelected bid selected = do
userId <- requireAuthId
runDB $ do
let k_bid = BookmarkKey bid
bm <- requireResource userId k_bid
update k_bid [BookmarkSelected =. selected]
pure ""
requireResource :: UserId -> Key Bookmark -> DBM Handler Bookmark
requireResource userId k_bid = do
bmark <- get404 k_bid
if userId == bookmarkUserId bmark
then return bmark
else notFound

12
src/Handler/Home.hs Normal file
View file

@ -0,0 +1,12 @@
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
module Handler.Home where
import Import
getHomeR :: Handler Html
getHomeR = do
musername <- maybeAuthUsername
case musername of
Nothing -> redirect (AuthR LoginR)
Just username -> redirect (UserR (UserNameP username))

134
src/Handler/Notes.hs Normal file
View file

@ -0,0 +1,134 @@
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
module Handler.Notes where
import Import
import Handler.Common (lookupPagingParams)
import qualified Data.Aeson as A
import qualified Data.Text as T
getNotesR :: UserNameP -> Handler Html
getNotesR unamep@(UserNameP uname) = do
void requireAuthId
(limit', page') <- lookupPagingParams
let queryp = "query" :: Text
mquery <- lookupGetParam queryp
let limit = maybe 20 fromIntegral limit'
page = maybe 1 fromIntegral page'
mqueryp = fmap (\q -> (queryp, q)) mquery
(bcount, notes) <-
runDB $
do Entity userId _ <- getBy404 (UniqueUserName uname)
getNoteList userId mquery limit page
req <- getRequest
mroute <- getCurrentRoute
defaultLayout $ do
let pager = $(widgetFile "pager")
search = $(widgetFile "search")
renderEl = "notes" :: Text
$(widgetFile "notes")
toWidgetBody [julius|
app.userR = "@{UserR unamep}";
app.dat.notes = #{ toJSON notes } || [];
|]
toWidget [julius|
PS['Main'].renderNotes('##{rawJS renderEl}')(app.dat.notes)();
|]
getNoteR :: UserNameP -> NtSlug -> Handler Html
getNoteR unamep@(UserNameP uname) slug = do
void requireAuthId
let renderEl = "note" :: Text
note <-
runDB $
do Entity userId _ <- getBy404 (UniqueUserName uname)
mnote <- getNote userId slug
maybe notFound pure mnote
defaultLayout $ do
addScript (StaticR js_marked_min_js)
$(widgetFile "note")
toWidgetBody [julius|
app.userR = "@{UserR unamep}";
app.dat.note = #{ toJSON note } || [];
|]
toWidget [julius|
PS['Main'].renderNote('##{rawJS renderEl}')(app.dat.note)();
|]
getAddNoteViewR :: UserNameP -> Handler Html
getAddNoteViewR unamep@(UserNameP uname) = do
userId <- requireAuthId
let renderEl = "note" :: Text
note <- liftIO $ Entity (NoteKey 0) <$> _toNote userId (NoteForm Nothing Nothing Nothing Nothing Nothing Nothing Nothing)
defaultLayout $ do
addScript (StaticR js_marked_min_js)
$(widgetFile "note")
toWidgetBody [julius|
app.userR = "@{UserR unamep}";
app.noteR = "@{NoteR unamep (noteSlug (entityVal note))}";
app.dat.note = #{ toJSON note } || [];
|]
toWidget [julius|
PS['Main'].renderNote('##{rawJS renderEl}')(app.dat.note)();
|]
deleteDeleteNoteR :: Int64 -> Handler Html
deleteDeleteNoteR nid = do
userId <- requireAuthId
runDB $ do
let k_nid = NoteKey nid
_ <- requireResource userId k_nid
deleteCascade k_nid
return ""
postAddNoteR :: Handler ()
postAddNoteR = do
noteForm <- requireCheckJsonBody
_handleFormSuccess noteForm >>= \case
(Created, nid) -> sendStatusJSON created201 nid
(Updated, _) -> sendResponseStatus noContent204 ()
requireResource :: UserId -> Key Note -> DBM Handler Note
requireResource userId k_nid = do
nnote <- get404 k_nid
if userId == noteUserId nnote
then return nnote
else notFound
_handleFormSuccess :: NoteForm -> Handler (UpsertResult, Key Note)
_handleFormSuccess noteForm = do
userId <- requireAuthId
note <- liftIO $ _toNote userId noteForm
runDB (upsertNote knid note)
where
knid = NoteKey <$> (_id noteForm >>= \i -> if i > 0 then Just i else Nothing)
data NoteForm = NoteForm
{ _id :: Maybe Int64
, _slug :: Maybe NtSlug
, _title :: Maybe Text
, _text :: Maybe Textarea
, _isMarkdown :: Maybe Bool
, _created :: Maybe UTCTimeStr
, _updated :: Maybe UTCTimeStr
} deriving (Show, Eq, Read, Generic)
instance FromJSON NoteForm where parseJSON = A.genericParseJSON gNoteFormOptions
instance ToJSON NoteForm where toJSON = A.genericToJSON gNoteFormOptions
gNoteFormOptions :: A.Options
gNoteFormOptions = A.defaultOptions { A.fieldLabelModifier = drop 1 }
_toNote :: UserId -> NoteForm -> IO Note
_toNote userId NoteForm {..} = do
time <- liftIO getCurrentTime
slug <- maybe mkNtSlug pure _slug
pure $
Note
userId
slug
(length _text)
(fromMaybe "" _title)
(maybe "" unTextarea _text)
(fromMaybe False _isMarkdown)
(fromMaybe time (fmap unUTCTimeStr _created))
(fromMaybe time (fmap unUTCTimeStr _updated))

62
src/Handler/User.hs Normal file
View file

@ -0,0 +1,62 @@
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
module Handler.User where
import Import
import qualified Data.Text as T
import Handler.Common (lookupPagingParams)
getUserR :: UserNameP -> Handler Html
getUserR uname@(UserNameP name) = do
_getUser uname SharedAll FilterAll (TagsP [])
getUserSharedR :: UserNameP -> SharedP -> Handler Html
getUserSharedR uname sharedp =
_getUser uname sharedp FilterAll (TagsP [])
getUserFilterR :: UserNameP -> FilterP -> Handler Html
getUserFilterR uname filterp =
_getUser uname SharedAll filterp (TagsP [])
getUserTagsR :: UserNameP -> TagsP -> Handler Html
getUserTagsR uname pathtags =
_getUser uname SharedAll FilterAll pathtags
_getUser :: UserNameP -> SharedP -> FilterP -> TagsP -> Handler Html
_getUser unamep@(UserNameP uname) sharedp' filterp' (TagsP pathtags) = do
mauthuname <- maybeAuthUsername
(limit', page') <- lookupPagingParams
let limit = maybe 120 fromIntegral limit'
page = maybe 1 fromIntegral page'
isowner = maybe False (== uname) mauthuname
sharedp = if isowner then sharedp' else SharedPublic
filterp = case filterp' of
FilterSingle _ -> filterp'
_ -> if isowner then filterp' else FilterAll
isAll = filterp == FilterAll && sharedp == SharedAll && pathtags == []
queryp = "query" :: Text
mquery <- lookupGetParam queryp
let mqueryp = fmap (\q -> (queryp, q)) mquery
(bcount, bmarks, alltags) <-
runDB $
do Entity userId user <- getBy404 (UniqueUserName uname)
when (not isowner && userPrivacyLock user)
(redirect (AuthR LoginR))
(cnt, bm) <- bookmarksQuery userId sharedp filterp pathtags mquery limit page
tg <- tagsQuery bm
pure (cnt, bm, tg)
when (bcount == 0) (case filterp of FilterSingle _ -> notFound; _ -> pure ())
mroute <- getCurrentRoute
req <- getRequest
defaultLayout $ do
let pager = $(widgetFile "pager")
search = $(widgetFile "search")
renderEl = "bookmarks" :: Text
$(widgetFile "user")
toWidgetBody [julius|
app.dat.bmarks = #{ toJSON $ toBookmarkFormList bmarks alltags } || [];
app.dat.isowner = #{ isowner };
app.userR = "@{UserR unamep}";
|]
toWidget [julius|
PS['Main'].renderBookmarks('##{rawJS renderEl}')(app.dat.bmarks)();
|]

86
src/Import.hs Normal file
View file

@ -0,0 +1,86 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
module Import
( module Import
) where
import Foundation as Import
import Import.NoFoundation as Import
import qualified Data.ByteString.Char8 as B8
import qualified Data.Aeson as A
-- Forms
type MonadHandlerForm m = (RenderMessage App FormMessage, HandlerSite m ~ App, MonadHandler m)
type Form f = Html -> MForm Handler (FormResult f, Widget)
runInputPostJSONResult
:: (FromJSON a, MonadHandlerForm m)
=> FormInput m a -> m (FormResult a)
runInputPostJSONResult form = do
mct <- lookupHeader "content-type"
case fmap (B8.takeWhile (/= ';')) mct of
Just "application/json" ->
parseJsonBody >>= \case
A.Success a -> pure $ FormSuccess a
A.Error e -> pure $ FormFailure [pack e]
Just "application/x-www-form-urlencoded" ->
runInputPostResult form
_ -> pure FormMissing
runInputPostJSON
:: (FromJSON a, MonadHandlerForm m)
=> FormInput m a -> m a
runInputPostJSON form =
runInputPostJSONResult form >>=
\case
FormSuccess a -> pure a
FormFailure e -> invalidArgs e
FormMissing -> invalidArgs []
class MkIForm a where
mkIForm :: MonadHandlerForm m => FormInput m a
aFormToMaybeGetSuccess
:: MonadHandler f
=> AForm f a -> f (Maybe a)
aFormToMaybeGetSuccess =
fmap maybeSuccess . fmap fst . runFormGet . const . fmap fst . aFormToForm
aFormToMaybePostSuccess
:: MonadHandlerForm f
=> AForm f a -> f (Maybe a)
aFormToMaybePostSuccess =
fmap maybeSuccess . fmap fst . runFormPostNoToken . const . fmap fst . aFormToForm
maybeSuccess :: FormResult a -> Maybe a
maybeSuccess (FormSuccess a) = Just a
maybeSuccess _ = Nothing
-- FieldSettings
named :: Text -> FieldSettings master -> FieldSettings master
named n f =
f
{ fsName = Just n
, fsId = Just n
}
attr :: (Text,Text) -> FieldSettings master -> FieldSettings master
attr n f =
f
{ fsAttrs = n : fsAttrs f
}
attrs :: [(Text,Text)] -> FieldSettings master -> FieldSettings master
attrs n f =
f
{ fsAttrs = n ++ fsAttrs f
}
cls :: [Text] -> FieldSettings master -> FieldSettings master
cls n = attrs [("class", intercalate " " n)]

View file

@ -0,0 +1,34 @@
{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -fno-warn-unused-imports #-}
module Import.NoFoundation
( module Import
#if MIN_VERSION_base(4, 11, 0)
#else
, (<&>)
#endif
) where
import ClassyPrelude.Yesod as Import
import Control.Monad.Trans.Maybe as Import
import Settings as Import
import Settings.StaticFiles as Import
import Yesod.Auth as Import
import Yesod.Core.Types as Import (loggerSet)
import Yesod.Default.Config2 as Import
import Text.Julius as Import
import Model as Import
import ModelCustom as Import
import Types as Import
import Pretty as Import
import Data.Functor as Import
import Generic as Import
#if MIN_VERSION_base(4, 11, 0)
#else
(<&>) :: Functor f => f a -> (a -> b) -> f b
as <&> f = f <$> as
infixl 1 <&>
#endif

569
src/Model.hs Normal file
View file

@ -0,0 +1,569 @@
{-# OPTIONS_GHC -fno-warn-unused-imports #-}
module Model where
import qualified ClassyPrelude.Yesod as CP
import qualified Data.Aeson as A
import qualified Data.Attoparsec.Text as P
import qualified Control.Monad.Combinators as PC
import qualified Data.List.NonEmpty as NE
import qualified Data.Time.ISO8601 as TI
import qualified Database.Esqueleto as E
import qualified Data.Time as TI
import ClassyPrelude.Yesod hiding ((||.))
import Control.Monad.Trans.Maybe
import Control.Monad.Writer (tell)
import Data.Char (isSpace)
import Data.Either (fromRight)
import Data.Foldable (foldl, foldl1, sequenceA_)
import Data.List.NonEmpty (NonEmpty(..))
import Database.Esqueleto hiding ((==.))
import Pretty
import System.Directory
import Types
import ModelCustom
share [mkPersist sqlSettings, mkDeleteCascade sqlSettings, mkMigrate "migrateSchema"] [persistLowerCase|
User json
Id Int64
name Text
passwordHash BCrypt
apiToken Text Maybe
privateDefault Bool
archiveDefault Bool
privacyLock Bool
UniqueUserName name
deriving Show Eq Typeable Ord
Bookmark json
Id Int64
userId UserId
slug BmSlug default="(lower(hex(randomblob(6))))"
href Text
description Text
extended Text
time UTCTime
shared Bool
toRead Bool
selected Bool
archiveHref Text Maybe
UniqueUserHref userId href
UniqueUserSlug userId slug
deriving Show Eq Typeable Ord
BookmarkTag json
Id Int64
userId UserId
tag Text
bookmarkId BookmarkId
seq Int
UniqueUserTagBookmarkId userId tag bookmarkId
UniqueUserBookmarkIdTagSeq userId bookmarkId tag seq
deriving Show Eq Typeable Ord
Note json
Id Int64
userId UserId
slug NtSlug default="(lower(hex(randomblob(10))))"
length Int
title Text
text Text
isMarkdown Bool
created UTCTime
updated UTCTime
deriving Show Eq Typeable Ord
|]
newtype UTCTimeStr =
UTCTimeStr { unUTCTimeStr :: UTCTime }
deriving (Eq, Show, Read, Generic, FromJSON, ToJSON)
instance PathPiece UTCTimeStr where
toPathPiece (UTCTimeStr u) = pack (TI.formatISO8601Millis u)
fromPathPiece s = UTCTimeStr <$> TI.parseISO8601 (unpack s)
newtype UserNameP =
UserNameP { unUserNameP :: Text }
deriving (Eq, Show, Read)
newtype TagsP =
TagsP { unTagsP :: [Text] }
deriving (Eq, Show, Read)
data SharedP
= SharedAll
| SharedPublic
| SharedPrivate
deriving (Eq, Show, Read)
data FilterP
= FilterAll
| FilterUnread
| FilterUntagged
| FilterStarred
| FilterSingle BmSlug
deriving (Eq, Show, Read)
newtype UnreadOnly =
UnreadOnly { unUnreadOnly :: Bool }
deriving (Eq, Show, Read)
type Limit = Int64
type Page = Int64
migrateAll :: Migration
migrateAll = migrateSchema >> migrateIndexes
dumpMigration :: DB ()
dumpMigration = printMigration migrateAll
runMigrations :: DB ()
runMigrations = runMigration migrateAll
toMigration :: [Text] -> Migration
toMigration = lift . tell . fmap (False ,)
migrateIndexes :: Migration
migrateIndexes =
toMigration
[ "CREATE INDEX IF NOT EXISTS idx_bookmark_time ON bookmark (user_id, time DESC)"
, "CREATE INDEX IF NOT EXISTS idx_bookmark_tag_bookmark_id ON bookmark_tag (bookmark_id, id, tag, seq)"
, "CREATE INDEX IF NOT EXISTS idx_note_user_created ON note (user_id, created DESC)"
]
authenticatePassword :: Text -> Text -> DB (Maybe (Entity User))
authenticatePassword username password = do
muser <- getBy (UniqueUserName username)
case muser of
Nothing -> return Nothing
Just dbuser ->
if validatePasswordHash (userPasswordHash (entityVal dbuser)) password
then return (Just dbuser)
else return Nothing
getUserByName :: UserNameP -> DB (Maybe (Entity User))
getUserByName (UserNameP uname) = do
selectFirst [UserName ==. uname] []
bookmarksQuery
:: Key User
-> SharedP
-> FilterP
-> [Tag]
-> Maybe Text
-> Limit
-> Page
-> DB (Int, [Entity Bookmark])
bookmarksQuery userId sharedp filterp tags mquery limit' page =
(,) -- total count
<$> fmap (sum . fmap E.unValue)
(select $
from $ \b -> do
_whereClause b
pure $ E.countRows)
-- paged data
<*> (select $
from $ \b -> do
_whereClause b
orderBy [desc (b ^. BookmarkTime)]
limit limit'
offset ((page - 1) * limit')
pure b)
where
_whereClause b = do
where_ $
foldl (\expr tag ->
expr &&. (exists $ -- each tag becomes an exists constraint
from $ \t ->
where_ (t ^. BookmarkTagBookmarkId E.==. b ^. BookmarkId &&.
(t ^. BookmarkTagTag `E.like` val tag))))
(b ^. BookmarkUserId E.==. val userId)
tags
case sharedp of
SharedAll -> pure ()
SharedPublic -> where_ (b ^. BookmarkShared E.==. val True)
SharedPrivate -> where_ (b ^. BookmarkShared E.==. val False)
case filterp of
FilterAll -> pure ()
FilterUnread -> where_ (b ^. BookmarkToRead E.==. val True)
FilterStarred -> where_ (b ^. BookmarkSelected E.==. val True)
FilterSingle slug -> where_ (b ^. BookmarkSlug E.==. val slug)
FilterUntagged -> where_ $ notExists $ from (\t -> where_ $
(t ^. BookmarkTagBookmarkId E.==. b ^. BookmarkId))
-- search
sequenceA_ (parseSearchQuery (toLikeExpr b) =<< mquery)
toLikeExpr :: E.SqlExpr (Entity Bookmark) -> Text -> E.SqlExpr (E.Value Bool)
toLikeExpr b term = fromRight p_allFields (P.parseOnly p_onefield term)
where
wild s = (E.%) ++. val s ++. (E.%)
toLikeB field s = b ^. field `E.like` wild s
p_allFields =
(toLikeB BookmarkHref term) ||.
(toLikeB BookmarkDescription term) ||.
(toLikeB BookmarkExtended term) ||.
(exists $ from (\t -> where_ $
(t ^. BookmarkTagBookmarkId E.==. b ^. BookmarkId) &&.
(t ^. BookmarkTagTag `E.like` (wild term))))
p_onefield = p_url <|> p_title <|> p_description <|> p_tags <|> p_after <|> p_before
where
p_url = "url:" *> fmap (toLikeB BookmarkHref) P.takeText
p_title = "title:" *> fmap (toLikeB BookmarkDescription) P.takeText
p_description = "description:" *> fmap (toLikeB BookmarkExtended) P.takeText
p_tags = "tags:" *> fmap (\term' -> exists $ from (\t -> where_ $
(t ^. BookmarkTagBookmarkId E.==. b ^. BookmarkId) &&.
(t ^. BookmarkTagTag `E.like` wild term'))) P.takeText
p_after = "after:" *> fmap ((b ^. BookmarkTime E.>=.) . val) (parseTimeText =<< P.takeText)
p_before = "before:" *> fmap ((b ^. BookmarkTime E.<=.) . val) (parseTimeText =<< P.takeText)
parseSearchQuery ::
(Text -> E.SqlExpr (E.Value Bool))
-> Text
-> Maybe (E.SqlQuery ())
parseSearchQuery toExpr =
fmap where_ . either (const Nothing) Just . P.parseOnly andE
where
andE = foldl1 (&&.) <$> P.many1 (P.skipSpace *> orE <|> tokenTermE)
orE = foldl1 (||.) <$> tokenTermE `P.sepBy1` P.char '|'
tokenTermE = negE termE <|> termE
where
negE p = not_ <$> (P.char '-' *> p)
termE = toExpr <$> (fieldTerm <|> quotedTerm <|> simpleTerm)
fieldTerm = concat <$> sequence [simpleTerm, P.string ":", quotedTerm <|> simpleTerm]
quotedTerm = PC.between (P.char '"') (P.char '"') (P.takeWhile1 (/= '"'))
simpleTerm = P.takeWhile1 (\c -> not (isSpace c) && c /= ':' && c /= '|')
parseTimeText :: (TI.ParseTime t, Monad m, Alternative m) => Text -> m t
parseTimeText t =
asum $
flip (parseTimeM True defaultTimeLocale) (unpack t) <$>
[ "%-m/%-d/%Y" , "%-m/%-d/%Y%z" , "%-m/%-d/%Y%Z" -- 12/31/2018
, "%Y-%-m-%-d" , "%Y-%-m-%-d%z" , "%Y-%-m-%-d%Z" -- 2018-12-31
, "%Y-%-m-%-dT%T" , "%Y-%-m-%-dT%T%z" , "%Y-%-m-%-dT%T%Z" -- 2018-12-31T06:40:53
, "%s" -- 1535932800
]
tagsQuery :: [Entity Bookmark] -> DB [Entity BookmarkTag]
tagsQuery bmarks =
select $
from $ \t -> do
where_ (t ^. BookmarkTagBookmarkId `in_` valList (fmap entityKey bmarks))
orderBy [asc (t ^. BookmarkTagSeq)]
pure t
withTags :: Key Bookmark -> DB [Entity BookmarkTag]
withTags key = selectList [BookmarkTagBookmarkId ==. key] [Asc BookmarkTagSeq]
-- Note List Query
getNote :: Key User -> NtSlug -> DB (Maybe (Entity Note))
getNote userKey slug =
selectFirst [NoteUserId ==. userKey, NoteSlug ==. slug] []
getNoteList :: Key User -> Maybe Text -> Limit -> Page -> DB (Int, [Entity Note])
getNoteList key mquery limit' page =
(,) -- total count
<$> fmap (sum . fmap E.unValue)
(select $
from $ \b -> do
_whereClause b
pure $ E.countRows)
<*> (select $
from $ \b -> do
_whereClause b
orderBy [desc (b ^. NoteCreated)]
limit limit'
offset ((page - 1) * limit')
pure b)
where
_whereClause b = do
where_ $ (b ^. NoteUserId E.==. val key)
-- search
sequenceA_ (parseSearchQuery (toLikeExpr b) =<< mquery)
toLikeExpr :: E.SqlExpr (Entity Note) -> Text -> E.SqlExpr (E.Value Bool)
toLikeExpr b term = fromRight p_allFields (P.parseOnly p_onefield term)
where
wild s = (E.%) ++. val s ++. (E.%)
toLikeN field s = b ^. field `E.like` wild s
p_allFields = toLikeN NoteTitle term ||. toLikeN NoteText term
p_onefield = p_title <|> p_text <|> p_after <|> p_before
where
p_title = "title:" *> fmap (toLikeN NoteTitle) P.takeText
p_text = "description:" *> fmap (toLikeN NoteText) P.takeText
p_after = "after:" *> fmap ((b ^. NoteCreated E.>=.) . val) (parseTimeText =<< P.takeText)
p_before = "before:" *> fmap ((b ^. NoteCreated E.<=.) . val) (parseTimeText =<< P.takeText)
-- Bookmark Files
bookmarkEntityToTags :: Entity Bookmark -> [Tag] -> [BookmarkTag]
bookmarkEntityToTags (Entity {entityKey = bookmarkId
,entityVal = Bookmark {..}}) tags =
fmap
(\(i, tag) -> BookmarkTag bookmarkUserId tag bookmarkId i)
(zip [1 ..] tags)
fileBookmarkToBookmark :: UserId -> FileBookmark -> IO Bookmark
fileBookmarkToBookmark user (FileBookmark {..}) = do
slug <- mkBmSlug
pure $
Bookmark
user
slug
fileBookmarkHref
fileBookmarkDescription
fileBookmarkExtended
fileBookmarkTime
fileBookmarkShared
fileBookmarkToRead
False
Nothing
insertFileBookmarks :: Key User -> FilePath -> DB ()
insertFileBookmarks userId bookmarkFile = do
mfmarks <- liftIO $ readFileBookmarks bookmarkFile
case mfmarks of
Left e -> print e
Right fmarks -> do
bookmarks <- liftIO $ mapM (fileBookmarkToBookmark userId) fmarks
mbookmarkIds <- mapM insertUnique bookmarks
let bookmarkTags =
concatMap (uncurry bookmarkEntityToTags) $
catMaybes $
zipWith3 (\mk v p -> map (\k -> (Entity k v, fileBookmarkTags p)) mk)
mbookmarkIds
bookmarks
fmarks
void $ mapM insertUnique bookmarkTags
where
readFileBookmarks :: MonadIO m => FilePath -> m (Either String [FileBookmark])
readFileBookmarks fpath = pure . A.eitherDecode' . fromStrict =<< readFile fpath
type Tag = Text
-- Notes
fileNoteToNote :: UserId -> FileNote -> IO Note
fileNoteToNote user (FileNote {..} ) = do
slug <- mkNtSlug
pure $
Note
user
slug
fileNoteLength
fileNoteTitle
fileNoteText
False
fileNoteCreatedAt
fileNoteUpdatedAt
insertDirFileNotes :: Key User -> FilePath -> DB ()
insertDirFileNotes userId noteDirectory = do
mfnotes <- liftIO $ readFileNotes noteDirectory
case mfnotes of
Left e -> print e
Right fnotes -> do
notes <- liftIO $ mapM (fileNoteToNote userId) fnotes
void $ mapM insertUnique notes
where
readFileNotes :: MonadIO m => FilePath -> m (Either String [FileNote])
readFileNotes fdir = do
files <- liftIO (listDirectory fdir)
noteBSS <- mapM (readFile . (fdir </>)) files
pure (mapM (A.eitherDecode' . fromStrict) noteBSS)
-- AccountSettingsForm
data AccountSettingsForm = AccountSettingsForm
{ _privateDefault :: Bool
, _archiveDefault :: Bool
, _privacyLock :: Bool
} deriving (Show, Eq, Read, Generic)
instance FromJSON AccountSettingsForm where parseJSON = A.genericParseJSON gDefaultFormOptions
instance ToJSON AccountSettingsForm where toJSON = A.genericToJSON gDefaultFormOptions
toAccountSettingsForm :: User -> AccountSettingsForm
toAccountSettingsForm (User {..}) =
AccountSettingsForm
{ _privateDefault = userPrivateDefault
, _archiveDefault = userArchiveDefault
, _privacyLock = userPrivacyLock
}
updateUserFromAccountSettingsForm :: Key User -> AccountSettingsForm -> DB ()
updateUserFromAccountSettingsForm userId (AccountSettingsForm {..}) = do
CP.update userId
[ UserPrivateDefault CP.=. _privateDefault
, UserArchiveDefault CP.=. _archiveDefault
, UserPrivacyLock CP.=. _privacyLock
]
-- BookmarkForm
data BookmarkForm = BookmarkForm
{ _url :: Text
, _title :: Maybe Text
, _description :: Maybe Textarea
, _tags :: Maybe Text
, _private :: Maybe Bool
, _toread :: Maybe Bool
, _bid :: Maybe Int64
, _slug :: Maybe BmSlug
, _selected :: Maybe Bool
, _time :: Maybe UTCTimeStr
, _archiveUrl :: Maybe Text
} deriving (Show, Eq, Read, Generic)
instance FromJSON BookmarkForm where parseJSON = A.genericParseJSON gDefaultFormOptions
instance ToJSON BookmarkForm where toJSON = A.genericToJSON gDefaultFormOptions
gDefaultFormOptions :: A.Options
gDefaultFormOptions = A.defaultOptions { A.fieldLabelModifier = drop 1 }
toBookmarkFormList :: [Entity Bookmark] -> [Entity BookmarkTag] -> [BookmarkForm]
toBookmarkFormList bs as = do
b <- bs
let bid = E.entityKey b
let btags = filter ((==) bid . bookmarkTagBookmarkId . E.entityVal) as
pure $ _toBookmarkForm (b, btags)
_toBookmarkForm :: (Entity Bookmark, [Entity BookmarkTag]) -> BookmarkForm
_toBookmarkForm (Entity bid Bookmark {..}, tags) =
BookmarkForm
{ _url = bookmarkHref
, _title = Just bookmarkDescription
, _description = Just $ Textarea $ bookmarkExtended
, _tags = Just $ unwords $ fmap (bookmarkTagTag . entityVal) tags
, _private = Just $ not bookmarkShared
, _toread = Just $ bookmarkToRead
, _bid = Just $ unBookmarkKey $ bid
, _slug = Just $ bookmarkSlug
, _selected = Just $ bookmarkSelected
, _time = Just $ UTCTimeStr $ bookmarkTime
, _archiveUrl = bookmarkArchiveHref
}
_toBookmark :: UserId -> BookmarkForm -> IO Bookmark
_toBookmark userId BookmarkForm {..} = do
time <- liftIO getCurrentTime
slug <- maybe mkBmSlug pure _slug
pure $
Bookmark
userId
slug
_url
(fromMaybe "" _title)
(maybe "" unTextarea _description)
(fromMaybe time (fmap unUTCTimeStr _time))
(maybe True not _private)
(fromMaybe False _toread)
(fromMaybe False _selected)
_archiveUrl
fetchBookmarkByUrl :: Key User -> Maybe Text -> DB (Maybe (Entity Bookmark, [Entity BookmarkTag]))
fetchBookmarkByUrl userId murl = runMaybeT $ do
bmark <- MaybeT . getBy . UniqueUserHref userId =<< (MaybeT $ pure murl)
btags <- lift $ withTags (entityKey bmark)
pure (bmark, btags)
data UpsertResult = Created | Updated
upsertBookmark:: Maybe (Key Bookmark) -> Bookmark -> [Text] -> DB (UpsertResult, Key Bookmark)
upsertBookmark mbid bm tags = do
res <- case mbid of
Just bid -> do
get bid >>= \case
Just prev_bm -> replaceBookmark bid prev_bm
_ -> fail "not found"
Nothing -> do
getBy (UniqueUserHref (bookmarkUserId bm) (bookmarkHref bm)) >>= \case
Just (Entity bid prev_bm) -> replaceBookmark bid prev_bm
_ -> (Created,) <$> insert bm
insertTags (bookmarkUserId bm) (snd res)
pure res
where
prepareReplace prev_bm = do
if (bookmarkHref bm /= bookmarkHref prev_bm)
then bm { bookmarkArchiveHref = Nothing }
else bm { bookmarkArchiveHref = bookmarkArchiveHref prev_bm }
replaceBookmark bid prev_bm = do
replace bid (prepareReplace prev_bm)
deleteTags bid
pure (Updated, bid)
deleteTags bid =
deleteWhere [BookmarkTagBookmarkId ==. bid]
insertTags userId bid' =
for_ (zip [1 ..] tags) $
\(i, tag) -> void $ insert $ BookmarkTag userId tag bid' i
updateBookmarkArchiveUrl :: Key User -> Key Bookmark -> Maybe Text -> DB ()
updateBookmarkArchiveUrl userId bid marchiveUrl = do
updateWhere
[BookmarkUserId ==. userId, BookmarkId ==. bid]
[BookmarkArchiveHref CP.=. marchiveUrl]
upsertNote:: Maybe (Key Note) -> Note -> DB (UpsertResult, Key Note)
upsertNote mnid bmark@Note{..} = do
case mnid of
Just nid -> do
get nid >>= \case
Just _ -> do
replace nid bmark
pure (Updated, nid)
_ -> fail "not found"
Nothing -> do
(Created,) <$> insert bmark
-- * FileBookmarks
data FileBookmark = FileBookmark
{ fileBookmarkHref :: !Text
, fileBookmarkDescription :: !Text
, fileBookmarkExtended :: !Text
, fileBookmarkTime :: !UTCTime
, fileBookmarkShared :: !Bool
, fileBookmarkToRead :: !Bool
, fileBookmarkTags :: [Tag]
} deriving (Show, Eq, Typeable, Ord)
instance FromJSON FileBookmark where
parseJSON (Object o) =
FileBookmark <$> o .: "href" <*> o .: "description" <*> o .: "extended" <*>
o .: "time" <*>
(boolFromYesNo <$> o .: "shared") <*>
(boolFromYesNo <$> o .: "toread") <*>
(words <$> o .: "tags")
parseJSON _ = fail "bad parse"
boolFromYesNo :: Text -> Bool
boolFromYesNo "yes" = True
boolFromYesNo _ = False
-- * FileNotes
data FileNote = FileNote
{ fileNoteId :: !Text
, fileNoteTitle :: !Text
, fileNoteText :: !Text
, fileNoteLength :: !Int
, fileNoteCreatedAt :: !UTCTime
, fileNoteUpdatedAt :: !UTCTime
} deriving (Show, Eq, Typeable, Ord)
instance FromJSON FileNote where
parseJSON (Object o) =
FileNote <$> o .: "id" <*> o .: "title" <*> o .: "text" <*>
o .: "length" <*>
(readFileNoteTime =<< o .: "created_at") <*>
(readFileNoteTime =<< o .: "updated_at")
parseJSON _ = fail "bad parse"
readFileNoteTime
:: Monad m
=> String -> m UTCTime
readFileNoteTime = parseTimeM True defaultTimeLocale "%F %T"

60
src/ModelCustom.hs Normal file
View file

@ -0,0 +1,60 @@
module ModelCustom where
import Prelude
import Crypto.BCrypt as Import hiding (hashPassword)
import Database.Persist.Sql
import Safe (fromJustNote)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Aeson as A
import System.Entropy (getEntropy)
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy as LBS
mkSlug :: Int -> IO T.Text
mkSlug size =
TE.decodeUtf8 . LBS.toStrict . BB.toLazyByteString . BB.byteStringHex <$>
getEntropy size
-- * Bookmark Slug
newtype BmSlug = BmSlug
{ unBmSlug :: T.Text
} deriving (Eq, PersistField, PersistFieldSql, Show, Read, Ord, A.FromJSON, A.ToJSON)
mkBmSlug :: IO BmSlug
mkBmSlug = BmSlug <$> mkSlug 6
-- * Note Slug
newtype NtSlug = NtSlug
{ unNtSlug :: T.Text
} deriving (Eq, PersistField, PersistFieldSql, Show, Read, Ord, A.FromJSON, A.ToJSON)
mkNtSlug :: IO NtSlug
mkNtSlug = NtSlug <$> mkSlug 10
-- * Model Crypto
policy :: HashingPolicy
policy =
HashingPolicy
{ preferredHashCost = 12
, preferredHashAlgorithm = "$2a$"
}
newtype BCrypt = BCrypt
{ unBCrypt :: T.Text
} deriving (Eq, PersistField, PersistFieldSql, Show, Ord, A.FromJSON, A.ToJSON)
hashPassword :: T.Text -> IO BCrypt
hashPassword rawPassword = do
mPassword <- hashPasswordUsingPolicy policy (TE.encodeUtf8 rawPassword)
return
(BCrypt (TE.decodeUtf8 (fromJustNote "Invalid hashing policy" mPassword)))
validatePasswordHash :: BCrypt -> T.Text -> Bool
validatePasswordHash hash' pass = do
validatePassword (TE.encodeUtf8 (unBCrypt hash')) (TE.encodeUtf8 pass)

55
src/PathPiece.hs Normal file
View file

@ -0,0 +1,55 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module PathPiece where
import Data.Text (splitOn)
import Import.NoFoundation
-- PathPiece
instance PathPiece UserNameP where
toPathPiece (UserNameP i) = "u:" <> i
fromPathPiece s =
case splitOn ":" s of
["u", ""] -> Nothing
["u", uname] -> Just $ UserNameP uname
_ -> Nothing
instance PathPiece TagsP where
toPathPiece (TagsP tags) = "t:" <> (intercalate "+" tags)
fromPathPiece s =
case splitOn ":" s of
["t", ""] -> Nothing
["t", tags] -> Just $ TagsP (splitOn "+" tags)
_ -> Nothing
instance PathPiece SharedP where
toPathPiece = \case
SharedAll -> ""
SharedPublic -> "public"
SharedPrivate -> "private"
fromPathPiece = \case
"public" -> Just SharedPublic
"private" -> Just SharedPrivate
_ -> Nothing
instance PathPiece FilterP where
toPathPiece = \case
FilterAll -> ""
FilterUnread -> "unread"
FilterUntagged -> "untagged"
FilterStarred -> "starred"
FilterSingle slug -> "b:" <> unBmSlug slug
fromPathPiece = \case
"unread" -> Just FilterUnread
"untagged" -> Just FilterUntagged
"starred" -> Just FilterStarred
s -> case splitOn ":" s of
["b", ""] -> Nothing
["b", slug] -> Just $ FilterSingle (BmSlug slug)
_ -> Nothing
deriving instance PathPiece NtSlug
deriving instance PathPiece BmSlug

15
src/Pretty.hs Normal file
View file

@ -0,0 +1,15 @@
module Pretty where
import Text.Show.Pretty (ppShow)
import Language.Haskell.HsColour
import Language.Haskell.HsColour.Colourise
import ClassyPrelude
cpprint :: (MonadIO m, Show a) => a -> m ()
cpprint = putStrLn . pack . hscolour TTY defaultColourPrefs False False "" False . ppShow
cprint :: (MonadIO m, Show a) => a -> m ()
cprint = putStrLn . pack . hscolour TTY defaultColourPrefs False False "" False . show
pprint :: (MonadIO m, Show a) => a -> m ()
pprint = putStrLn . pack . ppShow

150
src/Settings.hs Normal file
View file

@ -0,0 +1,150 @@
-- | Settings are centralized, as much as possible, into this file. This
-- includes database connection settings, static file locations, etc.
-- In addition, you can configure a number of different aspects of Yesod
-- by overriding methods in the Yesod typeclass. That instance is
-- declared in the Foundation.hs file.
module Settings where
import ClassyPrelude.Yesod
import qualified Control.Exception as Exception
import Data.Aeson (Result (..), fromJSON, withObject, (.!=),
(.:?))
import Data.FileEmbed (embedFile)
import Data.Yaml (decodeEither')
import Database.Persist.Sqlite (SqliteConf)
import Language.Haskell.TH.Syntax (Exp, Name, Q)
import Network.Wai.Handler.Warp (HostPreference)
import Yesod.Default.Config2 (applyEnvValue, configSettingsYml)
import Yesod.Default.Util (WidgetFileSettings, widgetFileNoReload,
widgetFileReload)
-- | Runtime settings to configure this application. These settings can be
-- loaded from various sources: defaults, environment variables, config files,
-- theoretically even a database.
data AppSettings = AppSettings
{ appStaticDir :: String
-- ^ Directory from which to serve static files.
, appDatabaseConf :: SqliteConf
-- ^ Configuration settings for accessing the database.
, appRoot :: Maybe Text
-- ^ Base for all generated URLs. If @Nothing@, determined
-- from the request headers.
, appHost :: HostPreference
-- ^ Host/interface the server should bind to.
, appPort :: Int
-- ^ Port to listen on
, appIpFromHeader :: Bool
-- ^ Get the IP address from the header when logging. Useful when sitting
-- behind a reverse proxy.
, appDetailedRequestLogging :: Bool
-- ^ Use detailed request logging system
, appShouldLogAll :: Bool
-- ^ Should all log messages be displayed?
, appReloadTemplates :: Bool
-- ^ Use the reload version of templates
, appMutableStatic :: Bool
-- ^ Assume that files in the static dir may change after compilation
, appSkipCombining :: Bool
-- ^ Perform no stylesheet/script combining
-- Example app-specific configuration values.
, appCopyright :: Text
-- ^ Copyright text to appear in the footer of the page
, appAnalytics :: Maybe Text
-- ^ Google Analytics code
, appAuthDummyLogin :: Bool
-- ^ Indicate if auth dummy login should be enabled.
, appEkgHost :: Maybe Text
-- ^ Host/interface the ekg server should bind to.
, appEkgPort :: Maybe Int
-- ^ Port to listen on
}
instance FromJSON AppSettings where
parseJSON = withObject "AppSettings" $ \o -> do
let defaultDev =
#ifdef DEVELOPMENT
True
#else
False
#endif
appStaticDir <- o .: "static-dir"
appDatabaseConf <- o .: "database"
appRoot <- o .:? "approot"
appHost <- fromString <$> o .: "host"
appPort <- o .: "port"
appIpFromHeader <- o .: "ip-from-header"
dev <- o .:? "development" .!= defaultDev
appDetailedRequestLogging <- o .:? "detailed-logging" .!= dev
appShouldLogAll <- o .:? "should-log-all" .!= dev
appReloadTemplates <- o .:? "reload-templates" .!= dev
appMutableStatic <- o .:? "mutable-static" .!= dev
appSkipCombining <- o .:? "skip-combining" .!= dev
appCopyright <- o .: "copyright"
appAnalytics <- o .:? "analytics"
appAuthDummyLogin <- o .:? "auth-dummy-login" .!= dev
appEkgHost <- o .:? "ekg-host"
appEkgPort <- o .:? "ekg-port"
return AppSettings {..}
-- | Settings for 'widgetFile', such as which template languages to support and
-- default Hamlet settings.
--
-- For more information on modifying behavior, see:
--
-- https://github.com/yesodweb/yesod/wiki/Overriding-widgetFile
widgetFileSettings :: WidgetFileSettings
widgetFileSettings = def
-- | How static files should be combined.
combineSettings :: CombineSettings
combineSettings = def
-- The rest of this file contains settings which rarely need changing by a
-- user.
widgetFile :: String -> Q Exp
widgetFile = (if appReloadTemplates compileTimeAppSettings
then widgetFileReload
else widgetFileNoReload)
widgetFileSettings
-- | Raw bytes at compile time of @config/settings.yml@
configSettingsYmlBS :: ByteString
configSettingsYmlBS = $(embedFile configSettingsYml)
-- | @config/settings.yml@, parsed to a @Value@.
configSettingsYmlValue :: Value
configSettingsYmlValue = either Exception.throw id
$ decodeEither' configSettingsYmlBS
-- | A version of @AppSettings@ parsed at compile time from @config/settings.yml@.
compileTimeAppSettings :: AppSettings
compileTimeAppSettings =
case fromJSON $ applyEnvValue False mempty configSettingsYmlValue of
Error e -> error e
Success settings -> settings
-- The following two functions can be used to combine multiple CSS or JS files
-- at compile time to decrease the number of http requests.
-- Sample usage (inside a Widget):
--
-- > $(combineStylesheets 'StaticR [style1_css, style2_css])
combineStylesheets :: Name -> [Route Static] -> Q Exp
combineStylesheets = combineStylesheets'
(appSkipCombining compileTimeAppSettings)
combineSettings
combineScripts :: Name -> [Route Static] -> Q Exp
combineScripts = combineScripts'
(appSkipCombining compileTimeAppSettings)
combineSettings

View file

@ -0,0 +1,18 @@
module Settings.StaticFiles where
import Settings (appStaticDir, compileTimeAppSettings)
import Yesod.Static (staticFiles)
-- This generates easy references to files in the static directory at compile time,
-- giving you compile-time verification that referenced files exist.
-- Warning: any files added to your static directory during run-time can't be
-- accessed this way. You'll have to use their FilePath or URL to access them.
--
-- For example, to refer to @static/js/script.js@ via an identifier, you'd use:
--
-- js_script_js
--
-- If the identifier is not available, you may use:
--
-- StaticFile ["js", "script.js"] []
staticFiles (appStaticDir compileTimeAppSettings)

13
src/Types.hs Normal file
View file

@ -0,0 +1,13 @@
module Types where
import ClassyPrelude.Yesod
type DBM m a = MonadUnliftIO m => SqlPersistT m a
type DB a = forall m. DBM m a
type DBVal val =
( PersistEntity val
, PersistEntityBackend val ~ SqlBackend
, PersistStore (PersistEntityBackend val))

11
stack.yaml Normal file
View file

@ -0,0 +1,11 @@
resolver: lts-13.0
# allow-newer: true
extra-deps:
- git: https://github.com/bitemyapp/esqueleto.git
commit: 5f98e7b25334ec120125ca84ef647d5c4575a010
- ekg-0.4.0.15
- ekg-json-0.1.0.6
- monad-metrics-0.2.1.2
- wai-middleware-metrics-0.2.4
packages:
- '.'

161
static/css/main.css Normal file
View file

@ -0,0 +1,161 @@
html {
height: 102%;
}
body {
height: 102%;
word-wrap: break-word;
}
button {
background:none;
border:none;
padding:0;
cursor:pointer;
}
button:focus {
outline: none;
}
[hidden] {
display: none !important
}
input::placeholder {
color: lightgray
}
.queryInput {
width: 128px;
padding: 0 22px 0 2px;
border-radius: 3px;
border-style: solid;
border-width: 1px;
border-color: gray;
height: 22px;
line-height: 22px;
transition: width .1s ease-in-out
}
.queryInput.search-inactive {}
.queryInput:focus {
width: 175px;
}
.submitting .queryInput,
.queryInput.search-active {
border-color: #990;
border-width: 2px;
background-color: #FF9;
width: 175px;
}
.queryIcon {
position: absolute;
right: 0;
top:1px;
cursor:pointer;
width:20px;
height: 20px;
fill: currentColor;
}
label {
cursor: pointer;
}
.close-x-wrap {
float: left;
width: 17px;
height: 17px;
top: 2px;
position: relative;
right: 2px;
}
.close-x {
stroke: gray;
fill: transparent;
stroke-linecap: round;
stroke-width: 3;
}
.query-info-icon {
position: absolute;
top: 0px;
right: -18px;
text-decoration: none;
font-size: 12px;
padding: 0 8px 8px 0;
}
.star {
margin-left:-20px;
font-size:1.2em;
position:relative;
top:-2px;
}
.star button {
transition: color .1s;
}
.star.selected button {
color:#22a;
}
.edit_links button {
transition: color .1s ease-in;
}
.tag {
color:#a51;
line-height:190%;
}
.private { background:#ddd;border:1px solid #d1d1d1; }
.unread { color:#b41 }
.mark_read {color: #a81;}
.flash { color:green;background:#efe }
.top_menu {
margin-top:6px;
}
.top_menu a {
color: blue;
}
.bookmarklet {
padding:1px 2px 0px 2px;
}
.alert {
background:#ced;
border:1px solid #acc;
}
.edit_bookmark_form {color:#888;}
.edit_bookmark_form input {border:1px solid #ddd;}
.edit_bookmark_form textarea {border:1px solid #ddd;}
.nav-active {
background:#ff8;
color:blue;
}
/* mobile device */
@media only screen and (max-width : 750px) {
body {
-webkit-text-size-adjust: none;
}
.display {
float: none
}
}
@media only screen and (max-width : 500px) {
.filters {
clear: both;
position: relative;
top: 2px;
}
}
.rdim {
opacity: .8;
transition: all .15s ease-in;
}
.rdim:hover,
.rdim:focus {
opacity: 1;
transition: all .15s ease-in;
}

35
static/css/popup.css Normal file
View file

@ -0,0 +1,35 @@
html {
box-sizing: border-box;
}
[hidden] {
display: none !important
}
button {
background:none;
border:none;
padding:0;
cursor:pointer;
}
button:focus {
outline: none;
}
.alert {
background:#ced;
border:1px solid #acc;
}
form label {
margin: 0;
vertical-align: middle;
display: table-cell;
padding: 2px 0;
}
li { list-style-type: none; margin: 0; padding: 0; display: block;}
.when { color:#999}
.unread { color:#b41 }
a.unread { color:#b41 }
a.bookmark_title { font-size:120%;}
label {
cursor: pointer;
}

3
static/css/tachyons.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,229 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata></metadata>
<defs>
<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
<font-face units-per-em="1200" ascent="960" descent="-240" />
<missing-glyph horiz-adv-x="500" />
<glyph />
<glyph />
<glyph unicode="&#xd;" />
<glyph unicode=" " />
<glyph unicode="*" d="M100 500v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259z" />
<glyph unicode="+" d="M0 400v300h400v400h300v-400h400v-300h-400v-400h-300v400h-400z" />
<glyph unicode="&#xa0;" />
<glyph unicode="&#x2000;" horiz-adv-x="652" />
<glyph unicode="&#x2001;" horiz-adv-x="1304" />
<glyph unicode="&#x2002;" horiz-adv-x="652" />
<glyph unicode="&#x2003;" horiz-adv-x="1304" />
<glyph unicode="&#x2004;" horiz-adv-x="434" />
<glyph unicode="&#x2005;" horiz-adv-x="326" />
<glyph unicode="&#x2006;" horiz-adv-x="217" />
<glyph unicode="&#x2007;" horiz-adv-x="217" />
<glyph unicode="&#x2008;" horiz-adv-x="163" />
<glyph unicode="&#x2009;" horiz-adv-x="260" />
<glyph unicode="&#x200a;" horiz-adv-x="72" />
<glyph unicode="&#x202f;" horiz-adv-x="260" />
<glyph unicode="&#x205f;" horiz-adv-x="326" />
<glyph unicode="&#x20ac;" d="M100 500l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406l-100 -100 h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217z" />
<glyph unicode="&#x2212;" d="M200 400h900v300h-900v-300z" />
<glyph unicode="&#x25fc;" horiz-adv-x="500" d="M0 0z" />
<glyph unicode="&#x2601;" d="M-14 494q0 -80 56.5 -137t135.5 -57h750q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5z" />
<glyph unicode="&#x2709;" d="M0 100l400 400l200 -200l200 200l400 -400h-1200zM0 300v600l300 -300zM0 1100l600 -603l600 603h-1200zM900 600l300 300v-600z" />
<glyph unicode="&#x270f;" d="M-13 -13l333 112l-223 223zM187 403l214 -214l614 614l-214 214zM887 1103l214 -214l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13z" />
<glyph unicode="&#xe001;" d="M0 1200h1200l-500 -550v-550h300v-100h-800v100h300v550z" />
<glyph unicode="&#xe002;" d="M14 84q18 -55 86 -75.5t147 5.5q65 21 109 69t44 90v606l600 155v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q18 -55 86 -75.5t147 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7 q-79 -25 -122.5 -82t-25.5 -112z" />
<glyph unicode="&#xe003;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" />
<glyph unicode="&#xe005;" d="M100 784q0 64 28 123t73 100.5t104.5 64t119 20.5t120 -38.5t104.5 -104.5q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5 t-94 124.5t-33.5 117.5z" />
<glyph unicode="&#xe006;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1z" />
<glyph unicode="&#xe007;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1zM237 700l196 -142l-73 -226l192 140l195 -141l-74 229l193 140h-235l-77 211l-78 -211h-239z" />
<glyph unicode="&#xe008;" d="M0 0v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100l400 -257v-143h-1200z" />
<glyph unicode="&#xe009;" d="M0 0v1100h1200v-1100h-1200zM100 100h100v100h-100v-100zM100 300h100v100h-100v-100zM100 500h100v100h-100v-100zM100 700h100v100h-100v-100zM100 900h100v100h-100v-100zM300 100h600v400h-600v-400zM300 600h600v400h-600v-400zM1000 100h100v100h-100v-100z M1000 300h100v100h-100v-100zM1000 500h100v100h-100v-100zM1000 700h100v100h-100v-100zM1000 900h100v100h-100v-100z" />
<glyph unicode="&#xe010;" d="M0 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM0 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5zM600 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM600 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe011;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 450v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe012;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5 t-14.5 -35.5v-200zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe013;" d="M29 454l419 -420l818 820l-212 212l-607 -607l-206 207z" />
<glyph unicode="&#xe014;" d="M106 318l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282l-212 -212l-282 282l-282 -282z" />
<glyph unicode="&#xe015;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233zM300 600v200h100v100h200v-100h100v-200h-100v-100h-200v100h-100z" />
<glyph unicode="&#xe016;" d="M23 694q0 200 142 342t342 142t342 -142t142 -342q0 -141 -78 -262l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 601h400v200h-400v-200z" />
<glyph unicode="&#xe017;" d="M23 600q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5 zM500 750q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400z" />
<glyph unicode="&#xe018;" d="M100 1h200v300h-200v-300zM400 1v500h200v-500h-200zM700 1v800h200v-800h-200zM1000 1v1200h200v-1200h-200z" />
<glyph unicode="&#xe019;" d="M26 601q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39l5 -2l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38 l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73zM385 601 q0 88 63 151t152 63t152 -63t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152z" />
<glyph unicode="&#xe020;" d="M100 1025v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18zM200 100v800h900v-800q0 -41 -29.5 -71t-70.5 -30h-700q-41 0 -70.5 30 t-29.5 71zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM500 1100h300v100h-300v-100zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" />
<glyph unicode="&#xe021;" d="M1 601l656 644l644 -644h-200v-600h-300v400h-300v-400h-300v600h-200z" />
<glyph unicode="&#xe022;" d="M100 25v1150q0 11 7 18t18 7h475v-500h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18zM700 800v300l300 -300h-300z" />
<glyph unicode="&#xe023;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 500v400h100 v-300h200v-100h-300z" />
<glyph unicode="&#xe024;" d="M-100 0l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538l-41 400h-242l-40 -400h-539zM488 500h224l-27 300h-170z" />
<glyph unicode="&#xe025;" d="M0 0v400h490l-290 300h200v500h300v-500h200l-290 -300h490v-400h-1100zM813 200h175v100h-175v-100z" />
<glyph unicode="&#xe026;" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM188 600q0 -170 121 -291t291 -121t291 121t121 291t-121 291t-291 121 t-291 -121t-121 -291zM350 600h150v300h200v-300h150l-250 -300z" />
<glyph unicode="&#xe027;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM350 600l250 300 l250 -300h-150v-300h-200v300h-150z" />
<glyph unicode="&#xe028;" d="M0 25v475l200 700h800l199 -700l1 -475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18zM200 500h200l50 -200h300l50 200h200l-97 500h-606z" />
<glyph unicode="&#xe029;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 397v401 l297 -200z" />
<glyph unicode="&#xe030;" d="M23 600q0 -118 45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123t123 184t45.5 224.5h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123 t-123 -184t-45.5 -224.5z" />
<glyph unicode="&#xe031;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150zM100 0v400h400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122z" />
<glyph unicode="&#xe032;" d="M100 0h1100v1200h-1100v-1200zM200 100v900h900v-900h-900zM300 200v100h100v-100h-100zM300 400v100h100v-100h-100zM300 600v100h100v-100h-100zM300 800v100h100v-100h-100zM500 200h500v100h-500v-100zM500 400v100h500v-100h-500zM500 600v100h500v-100h-500z M500 800v100h500v-100h-500z" />
<glyph unicode="&#xe033;" d="M0 100v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" />
<glyph unicode="&#xe034;" d="M100 0v1100h100v-1100h-100zM300 400q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500z" />
<glyph unicode="&#xe035;" d="M0 275q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5 t-49.5 -227v-300zM200 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14zM800 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14z" />
<glyph unicode="&#xe036;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM688 459l141 141l-141 141l71 71l141 -141l141 141l71 -71l-141 -141l141 -141l-71 -71l-141 141l-141 -141z" />
<glyph unicode="&#xe037;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" />
<glyph unicode="&#xe038;" d="M0 401v400h300l300 200v-800l-300 200h-300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257zM889 951l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8l81 -66l6 8q142 178 142 405q0 230 -144 408l-6 8z" />
<glyph unicode="&#xe039;" d="M0 0h500v500h-200v100h-100v-100h-200v-500zM0 600h100v100h400v100h100v100h-100v300h-500v-600zM100 100v300h300v-300h-300zM100 800v300h300v-300h-300zM200 200v100h100v-100h-100zM200 900h100v100h-100v-100zM500 500v100h300v-300h200v-100h-100v-100h-200v100 h-100v100h100v200h-200zM600 0v100h100v-100h-100zM600 1000h100v-300h200v-300h300v200h-200v100h200v500h-600v-200zM800 800v300h300v-300h-300zM900 0v100h300v-100h-300zM900 900v100h100v-100h-100zM1100 200v100h100v-100h-100z" />
<glyph unicode="&#xe040;" d="M0 200h100v1000h-100v-1000zM100 0v100h300v-100h-300zM200 200v1000h100v-1000h-100zM500 0v91h100v-91h-100zM500 200v1000h200v-1000h-200zM700 0v91h100v-91h-100zM800 200v1000h100v-1000h-100zM900 0v91h200v-91h-200zM1000 200v1000h200v-1000h-200z" />
<glyph unicode="&#xe041;" d="M0 700l1 475q0 10 7.5 17.5t17.5 7.5h474l700 -700l-500 -500zM148 953q0 -42 29 -71q30 -30 71.5 -30t71.5 30q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71z" />
<glyph unicode="&#xe042;" d="M1 700l1 475q0 11 7 18t18 7h474l700 -700l-500 -500zM148 953q0 -42 30 -71q29 -30 71 -30t71 30q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71zM701 1200h100l700 -700l-500 -500l-50 50l450 450z" />
<glyph unicode="&#xe043;" d="M100 0v1025l175 175h925v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900z" />
<glyph unicode="&#xe044;" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" />
<glyph unicode="&#xe045;" d="M0 100v700h200l100 -200h600l100 200h200v-700h-200v200h-800v-200h-200zM253 829l40 -124h592l62 124l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18zM281 24l38 152q2 10 11.5 17t19.5 7h500q10 0 19.5 -7t11.5 -17l38 -152q2 -10 -3.5 -17t-15.5 -7h-600 q-10 0 -15.5 7t-3.5 17z" />
<glyph unicode="&#xe046;" d="M0 200q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600z M356 500q0 100 72 172t172 72t172 -72t72 -172t-72 -172t-172 -72t-172 72t-72 172zM494 500q0 -44 31 -75t75 -31t75 31t31 75t-31 75t-75 31t-75 -31t-31 -75zM900 700v100h100v-100h-100z" />
<glyph unicode="&#xe047;" d="M53 0h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66zM416 521l178 457l46 -140l116 -317h-340 z" />
<glyph unicode="&#xe048;" d="M100 0v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21t-29 14t-49 14.5v71l471 -1q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111 t-162 -38.5h-500zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400zM400 700h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5v-379z" />
<glyph unicode="&#xe049;" d="M200 0v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500z" />
<glyph unicode="&#xe050;" d="M-75 200h75v800h-75l125 167l125 -167h-75v-800h75l-125 -167zM300 900v300h150h700h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49z " />
<glyph unicode="&#xe051;" d="M33 51l167 125v-75h800v75l167 -125l-167 -125v75h-800v-75zM100 901v300h150h700h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50z" />
<glyph unicode="&#xe052;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 350q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM0 650q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 950q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
<glyph unicode="&#xe053;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 650q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM200 350q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM200 950q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
<glyph unicode="&#xe054;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600 q-21 0 -35.5 15t-14.5 35z" />
<glyph unicode="&#xe055;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" />
<glyph unicode="&#xe056;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" />
<glyph unicode="&#xe057;" d="M-101 500v100h201v75l166 -125l-166 -125v75h-201zM300 0h100v1100h-100v-1100zM500 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35 v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 650q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100 q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100z" />
<glyph unicode="&#xe058;" d="M1 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 650 q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM801 0v1100h100v-1100 h-100zM934 550l167 -125v75h200v100h-200v75z" />
<glyph unicode="&#xe059;" d="M0 275v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53zM900 600l300 300v-600z" />
<glyph unicode="&#xe060;" d="M0 44v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31zM100 263l247 182l298 -131l-74 156l293 318l236 -288v500h-1000v-737zM208 750q0 56 39 95t95 39t95 -39t39 -95t-39 -95t-95 -39t-95 39t-39 95z " />
<glyph unicode="&#xe062;" d="M148 745q0 124 60.5 231.5t165 172t226.5 64.5q123 0 227 -63t164.5 -169.5t60.5 -229.5t-73 -272q-73 -114 -166.5 -237t-150.5 -189l-57 -66q-10 9 -27 26t-66.5 70.5t-96 109t-104 135.5t-100.5 155q-63 139 -63 262zM342 772q0 -107 75.5 -182.5t181.5 -75.5 q107 0 182.5 75.5t75.5 182.5t-75.5 182t-182.5 75t-182 -75.5t-75 -181.5z" />
<glyph unicode="&#xe063;" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM173 600q0 -177 125.5 -302t301.5 -125v854q-176 0 -301.5 -125 t-125.5 -302z" />
<glyph unicode="&#xe064;" d="M117 406q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 139t-64 210zM243 414q14 -82 59.5 -136 t136.5 -80l16 98q-7 6 -18 17t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156z" />
<glyph unicode="&#xe065;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125l200 200v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM436 341l161 50l412 412l-114 113l-405 -405zM995 1015l113 -113l113 113l-21 85l-92 28z" />
<glyph unicode="&#xe066;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5 zM423 524q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5z" />
<glyph unicode="&#xe067;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q61 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM342 632l283 -284l567 567l-137 137l-430 -431l-146 147z" />
<glyph unicode="&#xe068;" d="M0 603l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296l-300 -300v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198z" />
<glyph unicode="&#xe069;" d="M200 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe070;" d="M0 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe071;" d="M136 550l564 550v-487l500 487v-1100l-500 488v-488z" />
<glyph unicode="&#xe072;" d="M200 0l900 550l-900 550v-1100z" />
<glyph unicode="&#xe073;" d="M200 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800zM600 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
<glyph unicode="&#xe074;" d="M200 150q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
<glyph unicode="&#xe075;" d="M0 0v1100l500 -487v487l564 -550l-564 -550v488z" />
<glyph unicode="&#xe076;" d="M0 0v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488z" />
<glyph unicode="&#xe077;" d="M300 0v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438z" />
<glyph unicode="&#xe078;" d="M100 250v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5zM100 500h1100l-550 564z" />
<glyph unicode="&#xe079;" d="M185 599l592 -592l240 240l-353 353l353 353l-240 240z" />
<glyph unicode="&#xe080;" d="M272 194l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1l-592 -591z" />
<glyph unicode="&#xe081;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h200v-200h200v200h200v200h-200v200h-200v-200h-200v-200z" />
<glyph unicode="&#xe082;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h600v200h-600v-200z" />
<glyph unicode="&#xe083;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM246 459l213 -213l141 142l141 -142l213 213l-142 141l142 141l-213 212l-141 -141l-141 142l-212 -213l141 -141 z" />
<glyph unicode="&#xe084;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM270 551l276 -277l411 411l-175 174l-236 -236l-102 102z" />
<glyph unicode="&#xe085;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM364 700h143q4 0 11.5 -1t11 -1t6.5 3t3 9t1 11t3.5 8.5t3.5 6t5.5 4t6.5 2.5t9 1.5t9 0.5h11.5h12.5 q19 0 30 -10t11 -26q0 -22 -4 -28t-27 -22q-5 -1 -12.5 -3t-27 -13.5t-34 -27t-26.5 -46t-11 -68.5h200q5 3 14 8t31.5 25.5t39.5 45.5t31 69t14 94q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3q-50 0 -90.5 -12t-75 -38.5t-53.5 -74.5t-19 -114zM500 300h200v100h-200 v-100z" />
<glyph unicode="&#xe086;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM400 300h400v100h-100v300h-300v-100h100v-200h-100v-100zM500 800h200v100h-200v-100z" />
<glyph unicode="&#xe087;" d="M0 500v200h195q31 125 98.5 199.5t206.5 100.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h203v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194zM290 500q24 -73 79.5 -127.5t130.5 -78.5v206h200v-206 q149 48 201 206h-201v200h200q-25 74 -75.5 127t-124.5 77v-204h-200v203q-75 -23 -130 -77t-79 -126h209v-200h-210z" />
<glyph unicode="&#xe088;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM356 465l135 135 l-135 135l109 109l135 -135l135 135l109 -109l-135 -135l135 -135l-109 -109l-135 135l-135 -135z" />
<glyph unicode="&#xe089;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM322 537l141 141 l87 -87l204 205l142 -142l-346 -345z" />
<glyph unicode="&#xe090;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -115 62 -215l568 567q-100 62 -216 62q-171 0 -292.5 -121.5t-121.5 -292.5zM391 245q97 -59 209 -59q171 0 292.5 121.5t121.5 292.5 q0 112 -59 209z" />
<glyph unicode="&#xe091;" d="M0 547l600 453v-300h600v-300h-600v-301z" />
<glyph unicode="&#xe092;" d="M0 400v300h600v300l600 -453l-600 -448v301h-600z" />
<glyph unicode="&#xe093;" d="M204 600l450 600l444 -600h-298v-600h-300v600h-296z" />
<glyph unicode="&#xe094;" d="M104 600h296v600h300v-600h298l-449 -600z" />
<glyph unicode="&#xe095;" d="M0 200q6 132 41 238.5t103.5 193t184 138t271.5 59.5v271l600 -453l-600 -448v301q-95 -2 -183 -20t-170 -52t-147 -92.5t-100 -135.5z" />
<glyph unicode="&#xe096;" d="M0 0v400l129 -129l294 294l142 -142l-294 -294l129 -129h-400zM635 777l142 -142l294 294l129 -129v400h-400l129 -129z" />
<glyph unicode="&#xe097;" d="M34 176l295 295l-129 129h400v-400l-129 130l-295 -295zM600 600v400l129 -129l295 295l142 -141l-295 -295l129 -130h-400z" />
<glyph unicode="&#xe101;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM456 851l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5 t21.5 34.5l58 302q4 20 -8 34.5t-32 14.5h-207q-21 0 -33 -14.5t-8 -34.5zM500 300h200v100h-200v-100z" />
<glyph unicode="&#xe102;" d="M0 800h100v-200h400v300h200v-300h400v200h100v100h-111q1 1 1 6.5t-1.5 15t-3.5 17.5l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6 h-111v-100zM100 0h400v400h-400v-400zM200 900q-3 0 14 48t36 96l18 47l213 -191h-281zM700 0v400h400v-400h-400zM731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269z" />
<glyph unicode="&#xe103;" d="M0 -22v143l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55z M238.5 300.5q19.5 -6.5 86.5 76.5q55 66 367 234q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5z" />
<glyph unicode="&#xe104;" d="M111 408q0 -33 5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-5 -33 -20.5 -69.5t-37.5 -80.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5t30.5 2.5 t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5q2 -12 8 -41.5t8 -43t6 -39.5 t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85z" />
<glyph unicode="&#xe105;" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30l26 -40l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5 t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30zM120 600q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5t123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54 q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l105 105q-37 24 -75 72t-57 84l-20 36z" />
<glyph unicode="&#xe106;" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43zM120 600q210 -282 393 -336l37 141q-107 18 -178.5 101.5t-71.5 193.5 q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l47 47l23 87q-30 28 -59 69t-44 68l-14 26zM780 161l38 145q22 15 44.5 34t46 44t40.5 44t41 50.5t33.5 43.5t33 44t24.5 34q-97 127 -140 175l39 146q67 -54 131.5 -125.5t87.5 -103.5t36 -52l26 -40l-26 -40 q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5t-124 -100t-146.5 -79z" />
<glyph unicode="&#xe107;" d="M-97.5 34q13.5 -34 50.5 -34h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 34 -48 36.5t-48 -29.5l-642 -1066q-21 -32 -7.5 -66zM155 200l445 723l445 -723h-345v100h-200v-100h-345zM500 600l100 -300l100 300v100h-200v-100z" />
<glyph unicode="&#xe108;" d="M100 262v41q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -20 -13 -28.5t-32 0.5l-94 78h-222l-94 -78q-19 -9 -32 -0.5t-13 28.5 v64q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5z" />
<glyph unicode="&#xe109;" d="M0 50q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100v-750zM0 900h1100v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 100v100h100v-100h-100zM100 300v100h100v-100h-100z M100 500v100h100v-100h-100zM300 100v100h100v-100h-100zM300 300v100h100v-100h-100zM300 500v100h100v-100h-100zM500 100v100h100v-100h-100zM500 300v100h100v-100h-100zM500 500v100h100v-100h-100zM700 100v100h100v-100h-100zM700 300v100h100v-100h-100zM700 500 v100h100v-100h-100zM900 100v100h100v-100h-100zM900 300v100h100v-100h-100zM900 500v100h100v-100h-100z" />
<glyph unicode="&#xe110;" d="M0 200v200h259l600 600h241v198l300 -295l-300 -300v197h-159l-600 -600h-341zM0 800h259l122 -122l141 142l-181 180h-341v-200zM678 381l141 142l122 -123h159v198l300 -295l-300 -300v197h-241z" />
<glyph unicode="&#xe111;" d="M0 400v600q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5z" />
<glyph unicode="&#xe112;" d="M100 600v200h300v-250q0 -113 6 -145q17 -92 102 -117q39 -11 92 -11q37 0 66.5 5.5t50 15.5t36 24t24 31.5t14 37.5t7 42t2.5 45t0 47v25v250h300v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5t-89 96.5 t-58 109.5t-31.5 116t-15 104t-3 83zM100 900v300h300v-300h-300zM800 900v300h300v-300h-300z" />
<glyph unicode="&#xe113;" d="M-30 411l227 -227l352 353l353 -353l226 227l-578 579z" />
<glyph unicode="&#xe114;" d="M70 797l580 -579l578 579l-226 227l-353 -353l-352 353z" />
<glyph unicode="&#xe115;" d="M-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196zM402 1000l215 -200h381v-400h-198l299 -283l299 283h-200v600h-796z" />
<glyph unicode="&#xe116;" d="M18 939q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-300v-50q0 -20 -14.5 -35t-35.5 -15 t-35.5 15t-14.5 35v50h-50q-21 0 -35.5 15t-14.5 35q0 21 14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43z" />
<glyph unicode="&#xe117;" d="M0 0v800h1200v-800h-1200zM0 900v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-100h-1200z" />
<glyph unicode="&#xe118;" d="M1 0l300 700h1200l-300 -700h-1200zM1 400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000z" />
<glyph unicode="&#xe119;" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" />
<glyph unicode="&#xe120;" d="M0 600l300 298v-198h600v198l300 -298l-300 -297v197h-600v-197z" />
<glyph unicode="&#xe121;" d="M0 100v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM800 100h100v100h-100v-100z M1000 100h100v100h-100v-100z" />
<glyph unicode="&#xe122;" d="M-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5zM100 500v250v8v8v7t0.5 7t1.5 5.5t2 5t3 4t4.5 3.5t6 1.5t7.5 0.5h200l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35 q-55 337 -55 351zM1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35z" />
<glyph unicode="&#xe123;" d="M74 350q0 21 13.5 35.5t33.5 14.5h18l117 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94q20 0 29 -10.5t3 -29.5q-18 -36 -18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-8 -3 -23 -8.5 t-65 -20t-103 -25t-132.5 -19.5t-158.5 -9q-125 0 -245.5 20.5t-178.5 40.5l-58 20q-18 7 -31 27.5t-13 40.5zM497 110q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6t-103 6z" />
<glyph unicode="&#xe124;" d="M21 445l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180l-155 180l-45 -233l-224 78l78 -225l-233 -44l179 -156z" />
<glyph unicode="&#xe125;" d="M0 200h200v600h-200v-600zM300 275q0 -75 100 -75h61q124 -100 139 -100h250q46 0 83 57l238 344q29 31 29 74v100q0 44 -30.5 84.5t-69.5 40.5h-328q28 118 28 125v150q0 44 -30.5 84.5t-69.5 40.5h-50q-27 0 -51 -20t-38 -48l-96 -198l-145 -196q-20 -26 -20 -63v-400z M400 300v375l150 213l100 212h50v-175l-50 -225h450v-125l-250 -375h-214l-136 100h-100z" />
<glyph unicode="&#xe126;" d="M0 400v600h200v-600h-200zM300 525v400q0 75 100 75h61q124 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5h-50q-27 0 -51 20t-38 48l-96 198l-145 196 q-20 26 -20 63zM400 525l150 -212l100 -213h50v175l-50 225h450v125l-250 375h-214l-136 -100h-100v-375z" />
<glyph unicode="&#xe127;" d="M8 200v600h200v-600h-200zM308 275v525q0 17 14 35.5t28 28.5l14 9l362 230q14 6 25 6q17 0 29 -12l109 -112q14 -14 14 -34q0 -18 -11 -32l-85 -121h302q85 0 138.5 -38t53.5 -110t-54.5 -111t-138.5 -39h-107l-130 -339q-7 -22 -20.5 -41.5t-28.5 -19.5h-341 q-7 0 -90 81t-83 94zM408 289l100 -89h293l131 339q6 21 19.5 41t28.5 20h203q16 0 25 15t9 36q0 20 -9 34.5t-25 14.5h-457h-6.5h-7.5t-6.5 0.5t-6 1t-5 1.5t-5.5 2.5t-4 4t-4 5.5q-5 12 -5 20q0 14 10 27l147 183l-86 83l-339 -236v-503z" />
<glyph unicode="&#xe128;" d="M-101 651q0 72 54 110t139 38l302 -1l-85 121q-11 16 -11 32q0 21 14 34l109 113q13 12 29 12q11 0 25 -6l365 -230q7 -4 17 -10.5t26.5 -26t16.5 -36.5v-526q0 -13 -86 -93.5t-94 -80.5h-341q-16 0 -29.5 20t-19.5 41l-130 339h-107q-84 0 -139 39t-55 111zM-1 601h222 q15 0 28.5 -20.5t19.5 -40.5l131 -339h293l107 89v502l-343 237l-87 -83l145 -184q10 -11 10 -26q0 -11 -5 -20q-1 -3 -3.5 -5.5l-4 -4t-5 -2.5t-5.5 -1.5t-6.5 -1t-6.5 -0.5h-7.5h-6.5h-476v-100zM1000 201v600h200v-600h-200z" />
<glyph unicode="&#xe129;" d="M97 719l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53zM172 739l83 86l183 -146 q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6.5v7.5v6.5v456q0 22 25 31t50 -0.5t25 -30.5v-202q0 -16 20 -29.5t41 -19.5l339 -130v-294l-89 -100h-503zM400 0v200h600v-200h-600z" />
<glyph unicode="&#xe130;" d="M2 585q-16 -31 6 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85q0 -51 -0.5 -153.5t-0.5 -148.5q0 -84 38.5 -138t110.5 -54t111 55t39 139v106l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15zM77 565l236 339h503 l89 -100v-294l-340 -130q-20 -6 -40 -20t-20 -29v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146zM305 1104v200h600v-200h-600z" />
<glyph unicode="&#xe131;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM298 701l2 -201h300l-2 -194l402 294l-402 298v-197h-300z" />
<glyph unicode="&#xe132;" d="M0 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t231.5 47.5q122 0 232.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-218 -217.5t-300 -80t-299.5 80t-217.5 217.5t-80 299.5zM200 600l402 -294l-2 194h300l2 201h-300v197z" />
<glyph unicode="&#xe133;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600h200v-300h200v300h200l-300 400z" />
<glyph unicode="&#xe134;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600l300 -400l300 400h-200v300h-200v-300h-200z" />
<glyph unicode="&#xe135;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM254 780q-8 -33 5.5 -92.5t7.5 -87.5q0 -9 17 -44t16 -60 q12 0 23 -5.5t23 -15t20 -13.5q24 -12 108 -42q22 -8 53 -31.5t59.5 -38.5t57.5 -11q8 -18 -15 -55t-20 -57q42 -71 87 -80q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q104 -3 221 112q30 29 47 47t34.5 49t20.5 62q-14 9 -37 9.5t-36 7.5q-14 7 -49 15t-52 19q-9 0 -39.5 -0.5 t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5t5.5 57.5 q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5t34 21.5q-6 18 10 37q8 0 23.5 -1.5t24.5 -1.5t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 39 2 44q31 -13 58 -14.5t39 3.5l11 4q7 36 -16.5 53.5t-64.5 28.5t-56 23q-19 -3 -37 0 q-15 -12 -36.5 -21t-34.5 -12t-44 -8t-39 -6q-15 -3 -45.5 0.5t-45.5 -2.5q-21 -7 -52 -26.5t-34 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -90.5t-29.5 -79.5zM518 916q3 12 16 30t16 25q10 -10 18.5 -10t14 6t14.5 14.5t16 12.5q0 -24 17 -66.5t17 -43.5 q-9 2 -31 5t-36 5t-32 8t-30 14zM692 1003h1h-1z" />
<glyph unicode="&#xe136;" d="M0 164.5q0 21.5 15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138l145 -232l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5z" />
<glyph unicode="&#xe137;" horiz-adv-x="1220" d="M0 196v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 596v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5zM0 996v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM600 596h500v100h-500v-100zM800 196h300v100h-300v-100zM900 996h200v100h-200v-100z" />
<glyph unicode="&#xe138;" d="M100 1100v100h1000v-100h-1000zM150 1000h900l-350 -500v-300l-200 -200v500z" />
<glyph unicode="&#xe139;" d="M0 200v200h1200v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500z M500 1000h200v100h-200v-100z" />
<glyph unicode="&#xe140;" d="M0 0v400l129 -129l200 200l142 -142l-200 -200l129 -129h-400zM0 800l129 129l200 -200l142 142l-200 200l129 129h-400v-400zM729 329l142 142l200 -200l129 129v-400h-400l129 129zM729 871l200 200l-129 129h400v-400l-129 129l-200 -200z" />
<glyph unicode="&#xe141;" d="M0 596q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 596q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM291 655 q0 23 15.5 38.5t38.5 15.5t39 -16t16 -38q0 -23 -16 -39t-39 -16q-22 0 -38 16t-16 39zM400 850q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5zM514 609q0 32 20.5 56.5t51.5 29.5l122 126l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5 q22 0 38 -16t16 -39t-16 -39t-38 -16q-14 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-61.5 25.5t-25.5 61.5zM800 655q0 22 16 38t39 16t38.5 -15.5t15.5 -38.5t-16 -39t-38 -16q-23 0 -39 16t-16 39z" />
<glyph unicode="&#xe142;" d="M-40 375q-13 -95 35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -78.5 -16.5t-67.5 -51.5l-389 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23 q38 0 53 -36q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256l7 -7l69 -60 l517 511q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-40 -24 -111 -95l-512 -512q-68 -68 -81 -163z" />
<glyph unicode="&#xe143;" d="M80 784q0 131 98.5 229.5t230.5 98.5q143 0 241 -129q103 129 246 129q129 0 226 -98.5t97 -229.5q0 -46 -17.5 -91t-61 -99t-77 -89.5t-104.5 -105.5q-197 -191 -293 -322l-17 -23l-16 23q-43 58 -100 122.5t-92 99.5t-101 100q-71 70 -104.5 105.5t-77 89.5t-61 99 t-17.5 91zM250 784q0 -27 30.5 -70t61.5 -75.5t95 -94.5l22 -22q93 -90 190 -201q82 92 195 203l12 12q64 62 97.5 97t64.5 79t31 72q0 71 -48 119.5t-105 48.5q-74 0 -132 -83l-118 -171l-114 174q-51 80 -123 80q-60 0 -109.5 -49.5t-49.5 -118.5z" />
<glyph unicode="&#xe144;" d="M57 353q0 -95 66 -159l141 -142q68 -66 159 -66q93 0 159 66l283 283q66 66 66 159t-66 159l-141 141q-8 9 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159zM269 706q0 -93 66 -159l141 -141q7 -7 19 -17l105 105 l-212 212l389 389l247 -247l-95 -96l18 -17q47 -49 77 -100l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159z" />
<glyph unicode="&#xe145;" d="M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM300 300h600v700h-600v-700zM496 150q0 -43 30.5 -73.5t73.5 -30.5t73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5 t-73.5 -30.5t-30.5 -73.5z" />
<glyph unicode="&#xe146;" d="M0 0l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207z" />
<glyph unicode="&#xe148;" d="M295 433h139q5 -77 48.5 -126.5t117.5 -64.5v335q-6 1 -15.5 4t-11.5 3q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5 v-307l64 -14q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5zM466 889q0 -29 8 -51t16.5 -34t29.5 -22.5t31 -13.5t38 -10q7 -2 11 -3v274q-61 -8 -97.5 -37.5t-36.5 -102.5 zM700 237q170 18 170 151q0 64 -44 99.5t-126 60.5v-311z" />
<glyph unicode="&#xe149;" d="M100 600v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -28 16.5 -69.5t28 -62.5t41.5 -72h241v-100h-197q8 -50 -2.5 -115 t-31.5 -94q-41 -59 -99 -113q35 11 84 18t70 7q33 1 103 -16t103 -17q76 0 136 30l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5 t-30 142.5h-221z" />
<glyph unicode="&#xe150;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM602 900l298 300l298 -300h-198v-900h-200v900h-198z" />
<glyph unicode="&#xe151;" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v200h100v-100h200v-100h-300zM700 400v100h300v-200h-99v-100h-100v100h99v100h-200zM700 700v500h300v-500h-100v100h-100v-100h-100zM801 900h100v200h-100v-200z" />
<glyph unicode="&#xe152;" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v500h300v-500h-100v100h-100v-100h-100zM700 700v200h100v-100h200v-100h-300zM700 1100v100h300v-200h-99v-100h-100v100h99v100h-200zM801 200h100v200h-100v-200z" />
<glyph unicode="&#xe153;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 100v400h300v-500h-100v100h-200zM800 1100v100h200v-500h-100v400h-100zM901 200h100v200h-100v-200z" />
<glyph unicode="&#xe154;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 400v100h200v-500h-100v400h-100zM800 800v400h300v-500h-100v100h-200zM901 900h100v200h-100v-200z" />
<glyph unicode="&#xe155;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h500v-200h-500zM700 400v200h400v-200h-400zM700 700v200h300v-200h-300zM700 1000v200h200v-200h-200z" />
<glyph unicode="&#xe156;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h200v-200h-200zM700 400v200h300v-200h-300zM700 700v200h400v-200h-400zM700 1000v200h500v-200h-500z" />
<glyph unicode="&#xe157;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500z" />
<glyph unicode="&#xe158;" d="M0 400v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-163 0 -281.5 117.5t-118.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM400 300l333 250l-333 250v-500z" />
<glyph unicode="&#xe159;" d="M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 700l250 -333l250 333h-500z" />
<glyph unicode="&#xe160;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 400h500l-250 333z" />
<glyph unicode="&#xe161;" d="M0 400v300h300v200l400 -350l-400 -350v200h-300zM500 0v200h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-500v200h400q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-400z" />
<glyph unicode="&#xe162;" d="M217 519q8 -19 31 -19h302q-155 -438 -160 -458q-5 -21 4 -32l9 -8h9q14 0 26 15q11 13 274.5 321.5t264.5 308.5q14 19 5 36q-8 17 -31 17l-301 -1q1 4 78 219.5t79 227.5q2 15 -5 27l-9 9h-9q-15 0 -25 -16q-4 -6 -98 -111.5t-228.5 -257t-209.5 -237.5q-16 -19 -6 -41 z" />
<glyph unicode="&#xe163;" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q47 0 100 15v185h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h500v185q-14 4 -114 7.5t-193 5.5l-93 2q-165 0 -282.5 -117.5t-117.5 -282.5v-300zM600 400v300h300v200l400 -350l-400 -350v200h-300z " />
<glyph unicode="&#xe164;" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q163 0 281.5 117.5t118.5 282.5v98l-78 73l-122 -123v-148q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h156l118 122l-74 78h-100q-165 0 -282.5 -117.5t-117.5 -282.5 v-300zM496 709l353 342l-149 149h500v-500l-149 149l-342 -353z" />
<glyph unicode="&#xe165;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM406 600 q0 80 57 137t137 57t137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137z" />
<glyph unicode="&#xe166;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 800l445 -500l450 500h-295v400h-300v-400h-300zM900 150h100v50h-100v-50z" />
<glyph unicode="&#xe167;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 700h300v-300h300v300h295l-445 500zM900 150h100v50h-100v-50z" />
<glyph unicode="&#xe168;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 705l305 -305l596 596l-154 155l-442 -442l-150 151zM900 150h100v50h-100v-50z" />
<glyph unicode="&#xe169;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 988l97 -98l212 213l-97 97zM200 400l697 1l3 699l-250 -239l-149 149l-212 -212l149 -149zM900 150h100v50h-100v-50z" />
<glyph unicode="&#xe170;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM200 612l212 -212l98 97l-213 212zM300 1200l239 -250l-149 -149l212 -212l149 148l249 -237l-1 697zM900 150h100v50h-100v-50z" />
<glyph unicode="&#xe171;" d="M23 415l1177 784v-1079l-475 272l-310 -393v416h-392zM494 210l672 938l-672 -712v-226z" />
<glyph unicode="&#xe172;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-850q0 -21 -15 -35.5t-35 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200z" />
<glyph unicode="&#xe173;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-218l-276 -275l-120 120l-126 -127h-378v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM581 306l123 123l120 -120l353 352l123 -123l-475 -476zM600 1000h100v200h-100v-200z" />
<glyph unicode="&#xe174;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-269l-103 -103l-170 170l-298 -298h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200zM700 133l170 170l-170 170l127 127l170 -170l170 170l127 -128l-170 -169l170 -170 l-127 -127l-170 170l-170 -170z" />
<glyph unicode="&#xe175;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-300h-400v-200h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300l300 -300l300 300h-200v300h-200v-300h-200zM600 1000v200h100v-200h-100z" />
<glyph unicode="&#xe176;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-402l-200 200l-298 -298h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300h200v-300h200v300h200l-300 300zM600 1000v200h100v-200h-100z" />
<glyph unicode="&#xe177;" d="M0 250q0 -21 14.5 -35.5t35.5 -14.5h1100q21 0 35.5 14.5t14.5 35.5v550h-1200v-550zM0 900h1200v150q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 300v200h400v-200h-400z" />
<glyph unicode="&#xe178;" d="M0 400l300 298v-198h400v-200h-400v-198zM100 800v200h100v-200h-100zM300 800v200h100v-200h-100zM500 800v200h400v198l300 -298l-300 -298v198h-400zM800 300v200h100v-200h-100zM1000 300h100v200h-100v-200z" />
<glyph unicode="&#xe179;" d="M100 700v400l50 100l50 -100v-300h100v300l50 100l50 -100v-300h100v300l50 100l50 -100v-400l-100 -203v-447q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447zM800 597q0 -29 10.5 -55.5t25 -43t29 -28.5t25.5 -18l10 -5v-397q0 -21 14.5 -35.5 t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v1106q0 31 -18 40.5t-44 -7.5l-276 -116q-25 -17 -43.5 -51.5t-18.5 -65.5v-359z" />
<glyph unicode="&#xe180;" d="M100 0h400v56q-75 0 -87.5 6t-12.5 44v394h500v-394q0 -38 -12.5 -44t-87.5 -6v-56h400v56q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5v888q0 22 25 34.5t50 13.5l25 2v56h-400v-56q75 0 87.5 -6t12.5 -44v-394h-500v394q0 38 12.5 44t87.5 6v56h-400v-56q4 0 11 -0.5 t24 -3t30 -7t24 -15t11 -24.5v-888q0 -22 -25 -34.5t-50 -13.5l-25 -2v-56z" />
<glyph unicode="&#xe181;" d="M0 300q0 -41 29.5 -70.5t70.5 -29.5h300q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-300q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM100 100h400l200 200h105l295 98v-298h-425l-100 -100h-375zM100 300v200h300v-200h-300zM100 600v200h300v-200h-300z M100 1000h400l200 -200v-98l295 98h105v200h-425l-100 100h-375zM700 402v163l400 133v-163z" />
<glyph unicode="&#xe182;" d="M16.5 974.5q0.5 -21.5 16 -90t46.5 -140t104 -177.5t175 -208q103 -103 207.5 -176t180 -103.5t137 -47t92.5 -16.5l31 1l163 162q17 18 13.5 41t-22.5 37l-192 136q-19 14 -45 12t-42 -19l-118 -118q-142 101 -268 227t-227 268l118 118q17 17 20 41.5t-11 44.5 l-139 194q-14 19 -36.5 22t-40.5 -14l-162 -162q-1 -11 -0.5 -32.5z" />
<glyph unicode="&#xe183;" d="M0 50v212q0 20 10.5 45.5t24.5 39.5l365 303v50q0 4 1 10.5t12 22.5t30 28.5t60 23t97 10.5t97 -10t60 -23.5t30 -27.5t12 -24l1 -10v-50l365 -303q14 -14 24.5 -39.5t10.5 -45.5v-212q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-20 0 -35 14.5t-15 35.5zM0 712 q0 -21 14.5 -33.5t34.5 -8.5l202 33q20 4 34.5 21t14.5 38v146q141 24 300 24t300 -24v-146q0 -21 14.5 -38t34.5 -21l202 -33q20 -4 34.5 8.5t14.5 33.5v200q-6 8 -19 20.5t-63 45t-112 57t-171 45t-235 20.5q-92 0 -175 -10.5t-141.5 -27t-108.5 -36.5t-81.5 -40 t-53.5 -36.5t-31 -27.5l-9 -10v-200z" />
<glyph unicode="&#xe184;" d="M100 0v100h1100v-100h-1100zM175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250z" />
<glyph unicode="&#xe185;" d="M100 0h300v400q0 41 -29.5 70.5t-70.5 29.5h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-400zM500 0v1000q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-1000h-300zM900 0v700q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-700h-300z" />
<glyph unicode="&#xe186;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
<glyph unicode="&#xe187;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h100v200h100v-200h100v500h-100v-200h-100v200h-100v-500zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
<glyph unicode="&#xe188;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v100h-200v300h200v100h-300v-500zM600 300h300v100h-200v300h200v100h-300v-500z" />
<glyph unicode="&#xe189;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 550l300 -150v300zM600 400l300 150l-300 150v-300z" />
<glyph unicode="&#xe190;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300v500h700v-500h-700zM300 400h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130v-300zM575 549 q0 -65 27 -107t68 -42h130v300h-130q-38 0 -66.5 -43t-28.5 -108z" />
<glyph unicode="&#xe191;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
<glyph unicode="&#xe192;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v400h-200v100h-100v-500zM301 400v200h100v-200h-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
<glyph unicode="&#xe193;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 700v100h300v-300h-99v-100h-100v100h99v200h-200zM201 300v100h100v-100h-100zM601 300v100h100v-100h-100z M700 700v100h200v-500h-100v400h-100z" />
<glyph unicode="&#xe194;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 500v200 l100 100h300v-100h-300v-200h300v-100h-300z" />
<glyph unicode="&#xe195;" d="M0 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 400v400h300 l100 -100v-100h-100v100h-200v-100h200v-100h-200v-100h-100zM700 400v100h100v-100h-100z" />
<glyph unicode="&#xe197;" d="M-14 494q0 -80 56.5 -137t135.5 -57h222v300h400v-300h128q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200h200v300h200v-300h200 l-300 -300z" />
<glyph unicode="&#xe198;" d="M-14 494q0 -80 56.5 -137t135.5 -57h8l414 414l403 -403q94 26 154.5 104.5t60.5 178.5q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200l300 300 l300 -300h-200v-300h-200v300h-200z" />
<glyph unicode="&#xe199;" d="M100 200h400v-155l-75 -45h350l-75 45v155h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170z" />
<glyph unicode="&#xe200;" d="M121 700q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350l-75 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5 t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -11.5t1 -11.5q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5z" />
</font>
</defs></svg>

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Binary file not shown.

BIN
static/images/bluepin.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 B

14152
static/js/app.js Normal file

File diff suppressed because it is too large Load diff

BIN
static/js/app.js.gz Normal file

Binary file not shown.

1
static/js/app.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
static/js/app.min.js.gz Normal file

Binary file not shown.

4
static/js/html5shiv.min.js vendored Normal file
View file

@ -0,0 +1,4 @@
/**
* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
*/
!function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x<style>"+b+"</style>",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="<xyz></xyz>",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document);

Binary file not shown.

3
static/js/js.cookie-2.2.0.min.js vendored Normal file
View file

@ -0,0 +1,3 @@
/*! js-cookie v2.2.0 | MIT */
!function(e){var n=!1;if("function"==typeof define&&define.amd&&(define(e),n=!0),"object"==typeof exports&&(module.exports=e(),n=!0),!n){var o=window.Cookies,t=window.Cookies=e();t.noConflict=function(){return window.Cookies=o,t}}}(function(){function e(){for(var e=0,n={};e<arguments.length;e++){var o=arguments[e];for(var t in o)n[t]=o[t]}return n}function n(o){function t(n,r,i){var c;if("undefined"!=typeof document){if(arguments.length>1){if("number"==typeof(i=e({path:"/"},t.defaults,i)).expires){var a=new Date;a.setMilliseconds(a.getMilliseconds()+864e5*i.expires),i.expires=a}i.expires=i.expires?i.expires.toUTCString():"";try{c=JSON.stringify(r),/^[\{\[]/.test(c)&&(r=c)}catch(e){}r=o.write?o.write(r,n):encodeURIComponent(r+"").replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),n=(n=(n=encodeURIComponent(n+"")).replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent)).replace(/[\(\)]/g,escape);var s="";for(var f in i)i[f]&&(s+="; "+f,!0!==i[f]&&(s+="="+i[f]));return document.cookie=n+"="+r+s}n||(c={});for(var p=document.cookie?document.cookie.split("; "):[],d=/(%[0-9A-Z]{2})+/g,u=0;u<p.length;u++){var l=p[u].split("="),C=l.slice(1).join("=");this.json||'"'!==C.charAt(0)||(C=C.slice(1,-1));try{var m=l[0].replace(d,decodeURIComponent);if(C=o.read?o.read(C,m):o(C,m)||C.replace(d,decodeURIComponent),this.json)try{C=JSON.parse(C)}catch(e){}if(n===m){c=C;break}n||(c[m]=C)}catch(e){}}return c}}return t.set=t,t.get=function(e){return t.call(t,e)},t.getJSON=function(){return t.apply({json:!0},[].slice.call(arguments))},t.defaults={},t.remove=function(n,o){t(n,"",e(o,{expires:-1}))},t.withConverter=n,t}return n(function(){})});

Binary file not shown.

6
static/js/marked.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
static/js/marked.min.js.gz Normal file

Binary file not shown.

1
static/js/moment.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
static/js/moment.min.js.gz Normal file

Binary file not shown.

View file

@ -0,0 +1,18 @@
<main #main_column .pv2.ph3.mh1>
<div .w-100.mw8.center>
<div .measure.center.pa3.bg-white.ba.br2.b--black-10>
<form method="post" action="@{ChangePasswordR}">
$maybe token <- reqToken req
<input type="hidden" name="#{defaultCsrfParamName}" value="#{token}">
<div>
<label .db.fw6.lh-copy.f6 for="oldpassword">Old Password
<input #oldpasword .w-100.pa1.mb2.ba.b--black-20 autofocus required name="oldpassword" type="password" value="">
<div>
<label .db.fw6.lh-copy.f6 for="newpassword">New Password
<input #newpassword .w-100.pa1.mb2.ba.b--black-20 required name="newpassword" type="password">
<input class="ph3 pv2 input-reset ba b--navy bg-transparent pointer f6 dib mt3 dim" type="submit" value="Save Changes">

View file

@ -0,0 +1,37 @@
$newline never
\<!doctype html>
\<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en"> <![endif]-->
\<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en"> <![endif]-->
\<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en"> <![endif]-->
\<!--[if gt IE 8]><!-->
<html class="no-js" lang="en"> <!--<![endif]-->
<head>
<meta charset="UTF-8">
<title>#{pageTitle pc}
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width,initial-scale=1">
^{pageHead pc}
\<!--[if lt IE 9]>
\<script src="@{StaticR js_html5shiv_min_js}"></script>
\<![endif]-->
<script>document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/, 'js');
<script src="@{StaticR js_js_cookie_2_2_0_min_js}">
<script>
var app =
{ csrfHeaderName: "#{ TE.decodeUtf8 $ CI.foldedCase defaultCsrfHeaderName }"
, csrfParamName: "#{ defaultCsrfParamName }"
, csrfCookieName: "#{ TE.decodeUtf8 defaultCsrfCookieName }"
, csrfToken: Cookies.get("#{ TE.decodeUtf8 defaultCsrfCookieName }")
, homeR: "@{ HomeR }"
, authRlogoutR: "@{ AuthR LogoutR }"
, userFilterRFilterSingle: ""
, dat: {bmarks : [], bmark: {}, isowner: false, notes: []}
};
<body .f6.dark-gray.helvetica>
^{pageBody pc}

View file

@ -0,0 +1,34 @@
<div #content>
<header #banner .pv2.ph3.mh1>
<div .mw8.center.pb2.bb.br-0.bl-0.bt-0.b--dotted.b--light-silver>
<div #logo .fl.light-silver>
<a #espial_name .link.f4>espial
$maybe userName <- musername
<span>
(<a class="link" data-username="#{userName}" href="@{UserR (UserNameP userName)}">#{userName}</a>)
$maybe user <- muser
$if (userPrivacyLock user)
<a .dib.no-underline style="height:10px;width:10px" href="@{AccountSettingsR}" title="private profile enabled">🔒
<!-- <div #timer>#{pageLoadTime} s -->
<div .top_menu.fr>
$maybe userName <- musername
$maybe currentroute <- mcurrentRoute
<a .link href="@?{(AddViewR, [("next",urlrender currentroute)])}">add url&nbsp;&nbsp;
<a .link href="@{AddNoteViewR (UserNameP userName)}">add note&nbsp;&nbsp;
<a .link href="@{NotesR (UserNameP userName)}">notes&nbsp;&nbsp;
<a .link href="@{AccountSettingsR}">settings&nbsp;&nbsp;
<a .link onclick="PS['Main'].logoutE(event)()" href="@{AuthR LogoutR}">
log out
$nothing
<a .link href="@{AuthR LoginR}">
log in
<div .cf>
$maybe msg <- mmsg
<div .pv2.ph3.mh1>
<div .pa2.mw8.center.flex.items-center.justify-center.navy.bg-washed-yellow.ba.b--navy>
#{preEscapedToMarkup msg}
^{widget}

View file

@ -0,0 +1,86 @@
<main .pt2.pb5.mh1>
<h1 lh-title.mt0 style="font-size:1.35rem">Understanding the Search Syntax
<h3>Page filters
<p .ml3>Searches are scoped to the currently selected page filter.
<br>
<span>So, given the possible page filters</span>
<br><span class="code fw9 bg-light-gray">all ‧ private ‧ public ‧ unread ‧ untagged ‧ starred</span>
<br><span>If <span class="code fw9 bg-light-gray">all</span> is currently selected, the search includes <span class="code fw9 bg-light-gray">all</span> bookmarks</span>
<br><span>If <span class="code fw9 bg-light-gray">private</span> is currently selected, the search only includes <span class="code fw9 bg-light-gray">private</span> bookmarks, etc..</span>
<h3>Combine Searches (AND)
<p .ml3>Separate terms by a space.
<br>For example, <span class="code fw9 bg-light-gray">marathon race</span>
<h3>Combine Searches (OR)
<p .ml3>Put <span class="code fw9 bg-light-gray">|</span>&nbsp;between each search query.&nbsp;
<br>For example, <span class="code fw9 bg-light-gray">marathon|race</span>
<h3>Exclude words from your search
<p .ml3>Put <span class="code fw9 bg-light-gray">-</span>&nbsp;in front of a word you want to leave out.&nbsp;
<br>For example, <span class="code fw9 bg-light-gray">-car</span>
<h3>Search for an exact match
<p .ml3>Put a word or phrase inside quotes.&nbsp;
<br>For example, <span class="code fw9 bg-light-gray">"tallest building"</span>
<h3>Search on a specific field
<p .ml3>
Put <span class="code fw9 bg-light-gray"><span class="i">field</span>:</span> in front of the term,
where <span class="code fw9 bg-light-gray"><span class="i">field</span></span> is one of the options below
<div .ml3>
<div .f5.fw6.pb3.underline>Bookmark Search
<table .collapse.ba.br2.b--black-10.pv2.ph3>
<tbody>
<tr .striped--light-gray>
<td .pv2.ph3>FIELD
<td .pv2.ph3>EXAMPLE
<tr .striped--light-gray>
<td .pv2.ph3><span class="code fw9">url
<td .pv2.ph3><span class="code fw9">url:youtube.com
<tr .striped--light-gray>
<td .pv2.ph3><span class="code fw9">title
<td .pv2.ph3><span class="code fw9">title:"hacker news"
<tr .striped--light-gray>
<td .pv2.ph3><span class="code fw9">description
<td .pv2.ph3><span class="code fw9">description:surveys
<tr .striped--light-gray>
<td .pv2.ph3><span class="code fw9">tags
<td .pv2.ph3><span class="code fw9">tags:learning
<tr .striped--light-gray>
<td .pv2.ph3><span class="code fw9">after
<td .pv2.ph3><span class="code fw9">after:12/31/2018<br>after:2018-12-31
<tr .striped--light-gray>
<td .pv2.ph3><span class="code fw9">before
<td .pv2.ph3><span class="code fw9">before:12/31/2019<br>before:2019-12-31
<div .f5.fw6.pv3.underline>Note Search
<table .collapse.ba.br2.b--black-10.pv2.ph3>
<tbody>
<tr .striped--light-gray>
<td .pv2.ph3>FIELD
<td .pv2.ph3>EXAMPLE
<tr .striped--light-gray>
<td .pv2.ph3><span class="code fw9">title
<td .pv2.ph3><span class="code fw9">title:"hacker news"
<tr .striped--light-gray>
<td .pv2.ph3><span class="code fw9">description
<td .pv2.ph3><span class="code fw9">description:surveys
<tr .striped--light-gray>
<td .pv2.ph3><span class="code fw9">after
<td .pv2.ph3><span class="code fw9">after:12/31/2018<br>after:2018-12-31
<tr .striped--light-gray>
<td .pv2.ph3><span class="code fw9">before
<td .pv2.ph3><span class="code fw9">before:12/31/2019<br>before:2019-12-31
<h3 .mb0>More Complex Examples
<div .ml3.pt3>
<span>"youtube" in url, and title of "haskell" or title of "python" and after 12/31/2017
<br><span class="mw6 overflow-x-scroll nowrap db mt1 pa2 code fw9 bg-light-gray">url:youtube title:haskell|title:python after:12/31/2017
<div .ml3.pt3>
<span>"hacker news" not in title, and "news", "cnn", "npr" anywhere, or "the guardian" in the description
<br><span class="mw6 overflow-x-scroll nowrap db mt1 pa2 code fw9 bg-light-gray">-title:"hacker news" news|cnn|npr|description:"the guardian"

View file

@ -0,0 +1 @@
<div #main_column>

20
templates/login.hamlet Normal file
View file

@ -0,0 +1,20 @@
<main #main_column .pv2.ph3.mh1>
<div .w-100.mw8.center>
<div .measure.center.pa3.bg-white>
<form method="post" action="@{toParent dbLoginR}">
$maybe token <- reqToken req
<input type="hidden" name="#{defaultCsrfParamName}" value="#{token}">
<div>
<label .db.fw6.lh-copy.f6 for="username">Username
<input #username .w-100.pa1.mb2.ba.b--black-20 autofocus name="username" type="text" value="">
<div>
<label .db.fw6.lh-copy.f6 for="password">Password
<input #password .w-100.pa1.mb2.ba.b--black-20 name="password" type="password">
<input class="ph3 pv2 input-reset ba b--navy bg-transparent pointer f6 dib mt3 dim" type="submit" value="Log In">
<script> document.body.classList.add("bg-near-white");

6
templates/note.hamlet Normal file
View file

@ -0,0 +1,6 @@
<main #main_column .pv2.ph3.mh1>
<div .w-100.mw8.center>
<div ##{renderEl} .mt3>
<div .cf>

27
templates/notes.hamlet Normal file
View file

@ -0,0 +1,27 @@
<main #main_column .pv2.ph3.mh1>
<div .w-100.mw8.center>
<div .fr.nt1 style="margin-bottom:.7rem">
^{search}
<span .db .mb3>#{T.append "" (maybe "You have" (const "Found") mquery)} #{bcount} notes:
^{pager}
<div .cf>
<div ##{renderEl} .mt3>
<div .cf>
<div .user_footer hidden>
^{pager}
$if (fromIntegral bcount >= limit) || (page > 1)
$maybe route <- mroute
<div .dib.ml5>
<span .silver.mr1>per page:
<a .link.light-silver :limit == 20:.nav-active href="@?{(route, [("count", "20")])}"‧>20</a> ‧
<a .link.light-silver :limit == 40:.nav-active href="@?{(route, [("count", "40")])}"‧>40</a> ‧
<a .link.light-silver :limit == 80:.nav-active href="@?{(route, [("count", "80")])}"‧>80</a> ‧
<a .link.light-silver :limit == 120:.nav-active href="@?{(route, [("count", "120")])}"‧>120</a> ‧
<a .link.light-silver :limit == 160:.nav-active href="@?{(route, [("count", "160")])}"‧>160</a>

13
templates/pager.hamlet Normal file
View file

@ -0,0 +1,13 @@
$maybe route <- mroute
<div #nextprev style="border:0px solid orange">
<table style="float:left" border="0">
<tbody>
<tr>
$if fromIntegral bcount >= (limit * page)
<td width="80">
<a .link.gray #top_earlier href="@?{(route, catMaybes [Just ("page", pack (show (page + 1))), mqueryp])}">
« earlier
$if page > 1
<td width="80">
<a .link.gray #top_later href="@?{(route, catMaybes [Just ("page", pack (show (page - 1))), mqueryp])}">
later »

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