ῖon

This program was written by hand. It is currently pre-alpha, and undocumented. The following description was generated by Claude, when asked to describe the intent.


ῖon is a static site generator and document processing platform, distributed as a single POSIX shell script, that accepts virtually any input format — Word, LibreOffice, Markdown, LaTeX, reStructuredText, org-mode, Jupyter notebooks, AsciiDoc, HTML, EPUB, and dozens more — and produces optimised, accessible, standards-compliant static websites that work on every device, every browser, and every screen reader, with or without JavaScript. It is, more precisely, a media processing pipeline that happens to generate websites: point it at a directory of files and it scans, indexes, optimises images and video, processes documents, and publishes the result, with no migration step, no format conversion, and no vendor lock-in, because every supported input format is an open standard or a widely adopted one.

Nothing else in its category can do what ῖon does, or intends to do. WordPress, Squarespace, and Wix are monolithic, database-dependent, and proprietary, locking content into platforms that the author does not control. Hugo, Zola, and Eleventy are fast and capable but accept only Markdown or a handful of formats, hold the entire site in memory, run on a single machine, and have no concept of content relationships, cryptographic verification, or distributed builds. ῖon has no memory ceiling: its index is streamed from disk, meaning a corpus of ten files and a corpus of ten million files require the same amount of RAM. It scales horizontally without Kubernetes or any complex orchestration — each document is an independent unit of work, dispatched through GNU parallel across as many machines as are available over SSH, with no code changes and no coordination server. Incremental builds reprocess only what has changed, determined by cryptographic content hashing rather than timestamps, so a terabyte archive with a handful of daily edits rebuilds in seconds.

At the core of ῖon is a typed, content-addressed index with a full relationship graph: every file is tracked with its metadata, dimensions, hash, signature, authorship, and its relationships to other files — parent, translation, derivative, dependency, citation, series. A built-in query language traverses this graph at build time, enabling templates to ask questions like “all Arabic-language essays in this collection whose author has also published in the translations archive” without plugins, without a database, and without loading the corpus into memory. This is not a tag system or a taxonomy; it is a directed graph model with a query engine, closer to a lightweight document database than a static site generator feature.

A built-in inbox accepts content pushed via HTTP POST from any CMS, webhook, mobile application, or automated system, triggering an automatic rebuild — collapsing the traditional CMS-to-export-to-build-to-deploy pipeline into a single step: push content, get a website. Deployment reaches any storage backend through rclone: S3, Google Cloud, Azure, Backblaze, Cloudflare R2, SFTP, or on-premises storage. The entire tool runs on any machine with a POSIX shell — Linux, BSD, macOS, Alpine containers, embedded systems, a Raspberry Pi — with no runtime to install and no package manager to consult. It is extended through Lua filters operating on the document AST during processing, and through standard Unix tools operating on the filesystem index, following the Unix philosophy of small composable tools connected by pipes and files.

ῖon is designed for anyone who publishes documents: an individual running a personal site, a researcher managing a paper archive, a museum digitising its collection, a government publishing multilingual policy documents, a university consolidating heterogeneous departmental archives, a publisher processing thousands of manuscripts, an NGO coordinating documentation across dozens of languages and offices. Its real innovation is architectural: it decomposes the problem of processing a large heterogeneous corpus into an embarrassingly parallel workload that can be distributed across any number of machines with nothing more than SSH, opening the door to datasets of any size for indexing, cleaning, filtering, optimising, and publishing — at a scale and with a generality that no other static site generator, content management system, or website builder attempts.

The software is fully open-source under a custom licence derived from the SSPL: free to use, modify, and self-host without restriction; commercial use as a hosted service requires either full transparency of the service stack or a separate licence. It is at version 0.1.0 — early, with stubs and gaps — but its architecture, internal correctness, schema design, and pipeline structure are substantially complete, and nothing else in the field is built to occupy the same ground.


#!/bin/sh
# 
# ῖon
# ===
# 
# - 0.1.0; 2026-3-30 22:32; initial release
#
# References
# ==========
#
# - shellhaters.org
# - dylanaraps/pure-sh-bible
# - lua.org/manual/5.4/manual.html
# - pandoc.org/MANUAL.html#pandocs-markdown
# - pandoc.org/lua-filters.html
# 
# Licence
# =======
# 
# The source of this software is open to all, and free to all those
# who are open; use of this software to provide a service requires full
# transparency of the service stack, or otherwise a separate licence can
# be obtained.
# 
# This licence is based on the SSPL from MongoDB, Inc. which itself was
# derived from the AGPL from the Free Software Foundation. Section 18 was
# derived from the CLAs of the Apache Software Foundation and Project Harmony.
# The aim is to remain true to the spirit of free and open source software
# while supporting its long-term stability and reducing ambiguity for
# private enterprises.
# 
# 0. Definitions
# --------------
# 
# “The Author” refers to the copyright holder,
# or the rightful heirs and assigns thereof.
#
# “This Licence” refers to the Libra Public Licence,
# that is the licence attached to the head of this file.
# 
# “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 Licence. Each licensee is addressed as “you”.
# “Licensees” and “recipients” may be individuals or organisations.
# 
# To “modify” a work means to copy from, adapt, or translate all or part
# of the work in a fashion requiring copyright permission, including but
# not limited to the use of automated translation tools such as language
# models or other machine learning methods, 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
# Licence, and how to view a copy of this Licence. 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 recognised 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 Licence are granted for the term of
# copyright on the Program, and are irrevocable provided the stated
# conditions are met. This Licence explicitly affirms your unlimited
# permission to run the unmodified Program, subject to section 13. The
# output from running a covered work is covered by this Licence only if
# the output, given its content, constitutes a covered work. This Licence
# acknowledges your rights of fair use or other equivalent, as provided by
# copyright law.
# 
# Subject to section 13, you may make, run and propagate covered works
# that you do not convey, without conditions so long as your licence
# 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 Licence 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 Licence 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 Licence 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 Licence 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 Licence 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 Licence
#   to anyone who comes into possession of a copy. This Licence 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 Licence 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 Licence 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 Licence, 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 Licence, 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 favour 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, authorisation 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 characterised), 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
# Licence 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 Licence, 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
# Licence 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 Licence, for material you
# add to a covered work, you may (if authorised by the copyright holders
# of that material) supplement the terms of this Licence with terms:
# 
# - a) Disclaiming warranty or limiting liability differently from the
#   terms of sections 15 and 16 of this Licence; 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 Licence along with a term that is a further
# restriction, you may remove that term. If a licence document contains a
# further restriction but permits relicensing or conveying under this
# Licence, you may add to a covered work material governed by the terms of
# that licence 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 licence, 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 Licence. Any attempt otherwise to propagate or
# modify it is void, and will automatically terminate your rights under
# this Licence (including any patent licences granted under the third
# paragraph of section 11).
# 
# However, if you cease all violation of this Licence, then your licence
# from a particular copyright holder is reinstated (a) provisionally,
# unless and until the copyright holder explicitly and finally terminates
# your licence, 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 licence 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 Licence (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
# licences of parties who have received copies or rights from you under
# this Licence. If your rights have been terminated and not permanently
# reinstated, you do not qualify to receive new licences for the same
# material under section 10.
# 
# 9. Acceptance Not Required for Having Copies
# --------------------------------------------
# 
# You are not required to accept this Licence 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 Licence grants you permission to propagate or modify any covered
# work. These actions infringe copyright if you do not accept this
# Licence. Therefore, by modifying or propagating a covered work, you
# indicate your acceptance of this Licence to do so.
# 
# 10. Automatic Licensing of Downstream Recipients
# ------------------------------------------------
# 
# Each time you convey a covered work, the recipient automatically
# receives a licence from the original licensors, to run, modify and
# propagate that work, subject to this Licence. You are not responsible
# for enforcing compliance by third parties with this Licence.
# 
# An “entity transaction” is a transaction transferring control of an
# organisation, or substantially all assets of one, or subdividing an
# organisation, or merging organisations. 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 licences 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 Licence. For example, you may not
# impose a licence fee, royalty, or other charge for exercise of rights
# granted under this Licence, 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 authorises use under this
# Licence 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
# Licence, 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 sublicences in
# a manner consistent with the requirements of this Licence.
# 
# Each contributor grants you a non-exclusive, worldwide, royalty-free
# patent licence 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 licence” is any express
# agreement or commitment, however denominated, not to enforce a patent
# (such as an express permission to practise a patent or covenant not to
# sue for patent infringement). To “grant” such a patent licence 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 licence, and
# the Corresponding Source of the work is not available for anyone to
# copy, free of charge and under the terms of this Licence, 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 licence for this particular work, or (3) arrange, in a manner
# consistent with the requirements of this Licence, to extend the patent
# licence to downstream recipients. “Knowingly relying” means you have
# actual knowledge that, but for the patent licence, 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 licence to some of the parties
# receiving the covered work authorising them to use, propagate, modify or
# convey a specific copy of the covered work, then the patent licence you
# grant is automatically extended to all recipients of the covered work
# and works based on it.
# 
# A patent licence 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 Licence. 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 licence (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 licence was granted,
# prior to 28 March 2007.
# 
# Nothing in this Licence shall be construed as excluding or limiting any
# implied licence or other defences 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 Licence, they do not
# excuse you from the conditions of this Licence. If you cannot use,
# propagate or convey a covered work so as to satisfy simultaneously your
# obligations under this Licence and any other pertinent obligations, then
# as a consequence you may not use, propagate or 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 Licence would be to
# refrain entirely from conveying the Program.
# 
# 13. Offering the Program as a Service
# -------------------------------------
# 
# If you make the functionality of the Program or a modified version
# available to third parties as a service, you must either
# obtain a separate licence from the Author, or otherwise
# you must make the Service Source Code available via network download
# to everyone at no charge, under the terms of this Licence. Making the
# functionality of the Program or modified version available to third
# parties as a service includes, without limitation, enabling
# third parties to interact with the functionality of the Program or modified
# version remotely through a computer network, offering a service the value
# of which entirely or primarily derives from the value of the Program or
# modified version, or offering a service that accomplishes for users the
# primary purpose of the Program or modified version.
# 
# “Service Source Code” means the Corresponding Source for the Program or
# the modified version, and the Corresponding Source for all programs that
# you use to make the Program or modified version available as a service,
# including, without limitation, management software, user interfaces,
# application program interfaces, automation software, monitoring
# software, backup software, storage software and hosting software, all
# such that a user could run an instance of the service using the Service
# Source Code you make available.
# 
# 14. Revised Versions of this Licence
# ------------------------------------
# 
# The Author may publish revised and/or new versions of the Libra
# Public Licence 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 Libra Public
# Licence “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 Author. If the Program does not
# specify a version number of the Libra Public Licence, you may
# choose any version ever published by the Author.
# 
# If the Program specifies that a proxy can decide which future versions
# of the Libra Public Licence can be used, that proxy's public
# statement of acceptance of a version permanently authorises you to
# choose that version for the Program.
# 
# Later licence 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.
# 
# 18. Contributions
# -----------------
# 
# By submitting a contribution to this project, you grant the Author
# perpetual, worldwide, non-exclusive, no-charge, royalty-free, transferable,
# and irrevocable licence to import, use, reproduce, modify, prepare, display,
# perform, transfer, distribute, offer to sell, sell, and sublicence through
# multiple tiers of sublicensees, your contributions and such derivative works,
# including patent licences for any patent claims you own or control that are
# necessarily infringed by your contribution alone or in combination with
# this project.
# 
# By submitting a contribution to this project, you represent that you
# have the legal authority to grant the above licence, and that it is your
# original work or that you have all appropriate permissions, and you have fully
# disclosed its origin and any licence restrictions, including, but not
# limited to, related patents, trademarks, and any employer's
# intellectual property rights.
# 
# The Author provides no warranty regarding the acceptance or use of
# your contribution, which is given to the Author on an 'as is' basis,
# without warranties or conditions of any kind, either express or implied,
# including, without limitation, any warranties or conditions of title,
# non-infringement, merchantability, or fitness for a particular purpose.
# 
# Developers
# ==========
# 
# Due to the lack of variable scoping in POSIX shell scripts, all shell
# variables declared within functions are prefixed with a unique string,
# so that functions can call other functions without potentially getting
# their own variables overwritten. This wouldn't help in the case of
# recursive functions. This will likely be replaced with the local
# keyword at some point. The last prefix used was: ey

export ION___ERROR_PREFIX_MAIN="${ION___ERROR_PREFIX_MAIN:-"- "}"
export ION___ERROR_PREFIX_SUB="${ION___ERROR_PREFIX_SUB:-"  - "}"
export ION___ERROR_PREFIX_SUBL="${ION___ERROR_PREFIX_SUBL:-"["}"
export ION___ERROR_PREFIX_SUBR="${ION___ERROR_PREFIX_SUBR:-"] "}"
export ION___ERROR_INFIX_MAIN="${ION___ERROR_INFIX_MAIN:-": "}"
export ION___ERROR_INFIX_SUB="${ION___ERROR_INFIX_SUB:-"; "}"

export ION___TYPE_SEPARATOR="${ION___TYPE_SEPARATOR:-"."}"
export ION___TITLE_SEPARATOR="${ION___TITLE_SEPARATOR:-" – "}"

export ION___QUERY_PHRASE="${ION___QUERY_PHRASE:-"."}"
export ION___QUERY_CLAUSE="${ION___QUERY_CLAUSE:-":"}"
export ION___QUERY_SENTENCE="${ION___QUERY_SENTENCE:-"::"}"
export ION___QUERY_PARAGRAPH="${ION___QUERY_PARAGRAPH:-":::"}"

export ION___QUERY_ALL="${ION___QUERY_ALL:-"/"}"
export ION___QUERY_SEPARATOR="${ION___QUERY_SEPARATOR:-"?"}"
export ION___QUERY_NESTED_LEFT="${ION___QUERY_NESTED_LEFT:-"{"}"
export ION___QUERY_NESTED_RIGHT="${ION___QUERY_NESTED_RIGHT:-"}"}"

export ION___QUERY_SUBJECT="${ION___QUERY_SUBJECT:-"1"}"
export ION___QUERY_VERB="${ION___QUERY_VERB:-"2"}"

export ION___SUFFIX_SECONDS="${ION___SUFFIX_SECONDS:-"s"}"

export ION___SIGNAL_INPUT="${ION___SIGNAL_INPUT:-"i"}"
export ION___SIGNAL_SOURCE="${ION___SIGNAL_SOURCE:-"s"}"

export ION__EXT_JS="${ION__EXT_JS:-"js"}"
export ION__EXT_CSS="${ION__EXT_CSS:-"css"}"
export ION__EXT_HTML="${ION__EXT_HTML:-"html"}"
export ION__EXT_JSON="${ION__EXT_JSON:-"json"}"

export ION__WORD_INFO="${ION__WORD_INFO:-"info"}"
export ION__WORD_NOTE="${ION__WORD_NOTE:-"note"}"
export ION__WORD_ERROR="${ION__WORD_ERROR:-"error"}"
export ION__WORD_INBOX="${ION__WORD_INBOX:-"inbox"}"
export ION__WORD_INDEX="${ION__WORD_INDEX:-"index"}"
export ION__WORD_BUILD="${ION__WORD_BUILD:-"build"}"
export ION__WORD_SOURCE="${ION__WORD_SOURCE:-"source"}"
export ION__WORD_MAIN="${ION__WORD_MAIN:-"main"}"

export ION__NAME_PLAN="${ION__NAME_PLAN:-".$ION__WORD_BUILD"}"
export ION__NAME_ROOT="${ION__NAME_ROOT:-".$ION__WORD_INDEX"}"
export ION__NAME_BRANCH="${ION__NAME_BRANCH:-"$ION__WORD_INDEX"}"
export ION__NAME_INDEX_CSS="${ION__NAME_INDEX_CSS:-"$ION__WORD_INDEX.$ION__EXT_CSS"}"
export ION__NAME_INDEX_JS="${ION__NAME_INDEX_JS:-"$ION__WORD_INDEX.$ION__EXT_JS"}"
export ION__NAME_MAIN_CSS="${ION__NAME_MAIN_CSS:-"$ION__WORD_MAIN.$ION__EXT_CSS"}"
export ION__NAME_MAIN_JS="${ION__NAME_MAIN_JS:-"$ION__WORD_MAIN.$ION__EXT_JS"}"

export ION__MSG_CHANGING_DIR="${ION__MSG_CHANGING_DIR:-"changing the directory"}"
export ION__MSG_COMMAND_NOT_FOUND="${ION__MSG_COMMAND_NOT_FOUND:-"command not found"}"
export ION__MSG_COMMAND_NOT_EXEC="${ION__MSG_COMMAND_NOT_EXEC:-"command not executable"}"
export ION__MSG_COMMAND_NOT_RECOGNISED="${ION__MSG_COMMAND_NOT_RECOGNISED:-"command not recognised"}"
export ION__MSG_INVALID_ENVIRONMENT="${ION__MSG_INVALID_ENVIRONMENT:-"an invalid option was given"}"
export ION__MSG_INVALID_REPLACEMENT="${ION__MSG_INVALID_REPLACEMENT:-"a sed replacement contains a newline"}"
export ION__MSG_LINKING_FILE="${ION__MSG_LINKING_FILE:-"unable to create a hard link; reverting to copying"}"
export ION__MSG_NOT_SLEEPING="${ION__MSG_NOT_SLEEPING:-"sub-second sleep seemingly not possible"}"
export ION__MSG_CHANGED="${ION__MSG_CHANGED:-"waiting a moment..."}"
export ION__MSG_MAKING_DIR="${ION__MSG_MAKING_DIR:-"making a directory"}"
export ION__MSG_MAKING_FILE="${ION__MSG_MAKING_FILE:-"making a file"}"
export ION__MSG_MOVING_FILE="${ION__MSG_MOVING_FILE:-"moving a file"}"
export ION__MSG_NOT_POSIX="${ION__MSG_NOT_POSIX:-"the environment must be POSIX-compliant"}"
export ION__MSG_REMOVING_FILE="${ION__MSG_REMOVING_FILE:-"removing a file"}"
export ION__MSG_RUNNING_COMMAND="${ION__MSG_RUNNING_COMMAND:-"running command"}"
export ION__MSG_RUNNING_COMMAND_BG="${ION__MSG_RUNNING_COMMAND_BG:-"running background command"}"
export ION__MSG_RUNNING_COMMAND_MANY="${ION__MSG_RUNNING_COMMAND_MANY:-"running batch command"}"
export ION__MSG_RUNNING_TESTS="${ION__MSG_RUNNING_TESTS:-"running tests"}"
export ION__MSG_NOTICED_CHANGE="${ION__MSG_NOTICED_CHANGE:-"one moment..."}"
export ION__MSG_BUILD_STEP="${ION__MSG_BUILD_STEP:-"building"}"
export ION__MSG_BUILD_ACTION="${ION__MSG_BUILD_ACTION:-"building"}"
export ION__MSG_STARTING_COMPILER="${ION__MSG_STARTING_COMPILER:-"compiling"}"
export ION__MSG_STARTING_SERVERS="${ION__MSG_STARTING_SERVERS:-"starting the servers"}"
export ION__MSG_STARTING_WATCHER="${ION__MSG_STARTING_WATCHER:-"starting the watcher"}"
export ION__MSG_STOPPING_WATCHER="${ION__MSG_STOPPING_WATCHER:-"stopping the watcher"}"
export ION__MSG_STOPPING_SERVERS="${ION__MSG_STOPPING_SERVERS:-"stopping the servers"}"
export ION__MSG_STOPPING_BUILD="${ION__MSG_STOPPING_BUILD:-"finished"}"

export ION__MSG_OPENING_FILE="${ION__MSG_OPENING_FILE:-"opening a file"}"
export ION__MSG_QUERYING_THE_INDEX="${ION__MSG_QUERYING_THE_INDEX:-"querying the index"}"
export ION__MSG_QUERY_FOUND_AN_ENTRY="${ION__MSG_QUERY_FOUND_AN_ENTRY:-"found a matching entry in the index"}"

export ION__ACTION_SOURCE="${ION__ACTION_SOURCE:-"$ION__WORD_SOURCE"}"
export ION__ACTION_INDEX="${ION__ACTION_INDEX:-"$ION__WORD_INDEX"}"

export ION__VERB_IDENTITY="${ION__VERB_IDENTITY:-"identity"}"

export ION__TYPE_TRUE="${ION__TYPE_TRUE:-"true"}"
export ION__TYPE_FALSE="${ION__TYPE_FALSE:-"false"}"
export ION__TYPE_ARRAY="${ION__TYPE_ARRAY:-"array"}"
export ION__TYPE_OBJECT="${ION__TYPE_OBJECT:-"object"}"
export ION__TYPE_STRING="${ION__TYPE_STRING:-"string"}"
export ION__TYPE_NUMBER="${ION__TYPE_NUMBER:-"number"}"
export ION__TYPE_BOOLEAN="${ION__TYPE_BOOLEAN:-"boolean"}"
export ION__TYPE_NULL="${ION__TYPE_NULL:-"null"}"
export ION__TYPE_TEXT="${ION__TYPE_TEXT:-"text"}"
export ION__TYPE_PATH="${ION__TYPE_PATH:-"path"}"
export ION__TYPE_PATHS="${ION__TYPE_PATHS:-"paths"}"
export ION__TYPE_REFERENCE="${ION__TYPE_REFERENCE:-"reference"}"
export ION__TYPE_RELATIONSHIP="${ION__TYPE_RELATIONSHIP:-"relationship"}"

export ION__META_TYPE="${ION__META_TYPE:-"type"}"
export ION__META_TYPE_DIRECTORY="${ION__META_TYPE_DIRECTORY:-"directory"}"
export ION__META_TYPE_FILE="${ION__META_TYPE_FILE:-"file"}"
export ION__META_TYPE_DATA="${ION__META_TYPE_DATA:-"data"}"
export ION__META_TYPE_DOCUMENT="${ION__META_TYPE_DOCUMENT:-"document"}"
export ION__META_TYPE_CODE="${ION__META_TYPE_CODE:-"code"}"
export ION__META_TYPE_STYLE="${ION__META_TYPE_STYLE:-"style"}"
export ION__META_TYPE_SCRIPT="${ION__META_TYPE_SCRIPT:-"script"}"
export ION__META_TYPE_IMAGE="${ION__META_TYPE_IMAGE:-"image"}"
export ION__META_TYPE_AUDIO="${ION__META_TYPE_AUDIO:-"audio"}"
export ION__META_TYPE_VIDEO="${ION__META_TYPE_VIDEO:-"video"}"
export ION__META_TYPE_MAP="${ION__META_TYPE_MAP:-"map"}"
export ION__META_TYPE_OBJECT="${ION__META_TYPE_OBJECT:-"object"}"
export ION__META_TYPE_FONT="${ION__META_TYPE_FONT:-"font"}"

export ION__META_SCAN="${ION__META_SCAN:-"scan"}"

export ION__META_PATH="${ION__META_PATH:-"path"}"
export ION__META_NAME="${ION__META_NAME:-"name"}"
export ION__META_EXTENSION="${ION__META_EXTENSION:-"extension"}"
export ION__META_DOMAIN="${ION__META_DOMAIN:-"domain"}"

export ION__META_HASH="${ION__META_HASH:-"hash"}"
export ION__META_STAMP="${ION__META_STAMP:-"stamp"}"
export ION__META_SIGNER="${ION__META_SIGNER:-"signer"}"
export ION__META_SIGNATURE="${ION__META_SIGNATURE:-"signature"}"
export ION__META_SALT="${ION__META_SALT:-"salt"}"

export ION__META_LOCATION="${ION__META_LOCATION:-"location"}"
export ION__META_ITERATION="${ION__META_ITERATION:-"iteration"}"
export ION__META_MODIFIED="${ION__META_MODIFIED:-"modified"}"
export ION__META_SIZE="${ION__META_SIZE:-"size"}"

export ION__META_WIDTH="${ION__META_WIDTH:-"width"}"
export ION__META_HEIGHT="${ION__META_HEIGHT:-"height"}"
export ION__META_DEPTH="${ION__META_DEPTH:-"depth"}"
export ION__META_LENGTH="${ION__META_LENGTH:-"length"}"

export ION__META_DIRECTION="${ION__META_DIRECTION:-"direction"}"
export ION__META_ENCODING="${ION__META_ENCODING:-"encoding"}"
export ION__META_ALPHABET="${ION__META_ALPHABET:-"alphabet"}"
export ION__META_LANGUAGE="${ION__META_LANGUAGE:-"language"}"
export ION__META_WORDMARK="${ION__META_WORDMARK:-"wordmark"}"
export ION__META_FLAG="${ION__META_FLAG:-"flag"}"
export ION__META_ICON="${ION__META_ICON:-"icon"}"

export ION__META_TITLE="${ION__META_TITLE:-"title"}"
export ION__META_DESCRIPTION="${ION__META_DESCRIPTION:-"description"}"
export ION__META_PUBLISHED="${ION__META_PUBLISHED:-"published"}"
export ION__META_AUTHORS="${ION__META_AUTHORS:-"authors"}"
export ION__META_ALBUM="${ION__META_ALBUM:-"album"}"
export ION__META_PUBLIC="${ION__META_PUBLIC:-"public"}"
export ION__META_COVER="${ION__META_COVER:-"cover"}"

export ION__META_CATEGORY="${ION__META_CATEGORY:-"category"}"
export ION__META_LABELS="${ION__META_LABELS:-"labels"}"

export ION__META_FROM="${ION__META_FROM:-"from"}"
export ION__META_PARENT="${ION__META_PARENT:-"parent"}"
export ION__META_RELATED="${ION__META_RELATED:-"related"}"
export ION__META_PREVIOUSLY="${ION__META_PREVIOUSLY:-"previously"}"
export ION__META_REFERENCES="${ION__META_REFERENCES:-"references"}"
export ION__META_DEPENDENCIES="${ION__META_DEPENDENCIES:-"dependencies"}"
export ION__META_TRANSLATIONS="${ION__META_TRANSLATIONS:-"translations"}"
export ION__META_DERIVATIVES="${ION__META_DERIVATIVES:-"derivatives"}"

export ION__ATTR_CURRENT="${ION__ATTR_CURRENT:-"data-current"}"
export ION__ATTR_QUERY="${ION__ATTR_QUERY:-"data-query"}"
export ION__ATTR_MULTIPLE="${ION__ATTR_MULTIPLE:-"data-multiple"}"
export ION__ATTR_NESTED="${ION__ATTR_NESTED:-"data-nested"}"
export ION__ATTR_KEY="${ION__ATTR_KEY:-"data-key"}"
export ION__ATTR_TYPE="${ION__ATTR_TYPE:-"data-type"}"
export ION__ATTR_VALUE="${ION__ATTR_VALUE:-"data-value"}"
export ION__ATTR_NAME="${ION__ATTR_NAME:-"data-name"}"

export ION__CLASS_INDEX="${ION__CLASS_INDEX:-"index"}"
export ION__CLASS_WORD="${ION__CLASS_WORD:-"word"}"
export ION__CLASS_PAGE="${ION__CLASS_PAGE:-"page"}"
export ION__CLASS_FORM="${ION__CLASS_FORM:-"form"}"
export ION__CLASS_HEADER="${ION__CLASS_HEADER:-"header"}"
export ION__CLASS_REQUIRED="${ION__CLASS_REQUIRED:-"required"}"
export ION__CLASS_COMPONENT="${ION__CLASS_COMPONENT:-"component"}"
export ION__CLASS_NO_JS="${ION__CLASS_NO_JS:-"no-js"}"

export ION_BIN_SELF="${ION_BIN_SELF:-"$0"}"
export ION_BIN_FLOCK="${ION_BIN_FLOCK:-"flock"}"
export ION_BIN_FSWATCH="${ION_BIN_FSWATCH:-"fswatch"}"
export ION_BIN_CADDY="${ION_BIN_CADDY:-"caddy"}"
export ION_BIN_ESBUILD="${ION_BIN_ESBUILD:-"esbuild"}"
export ION_BIN_LN="${ION_BIN_LN:-"ln"}"
export ION_BIN_LUAC="${ION_BIN_LUAC:-"luac"}"
export ION_BIN_OPENSSL="${ION_BIN_OPENSSL:-"openssl"}"
export ION_BIN_PANDOC="${ION_BIN_PANDOC:-"pandoc"}"
export ION_BIN_RCLONE="${ION_BIN_RCLONE:-"rclone"}"
export ION_BIN_SHA256SUM="${ION_BIN_SHA256SUM:-"sha256sum"}"
export ION_BIN_SHA256="${ION_BIN_SHA256:-"sha256"}"
export ION_BIN_SHASUM="${ION_BIN_SHASUM:-"shasum"}"
export ION_BIN_SHELLCHECK="${ION_BIN_SHELLCHECK:-"shellcheck"}"
export ION_BIN_SSH="${ION_BIN_SSH:-"ssh"}"
export ION_BIN_TCPSERVER="${ION_BIN_TCPSERVER:-"tcpserver"}"
export ION_BIN_TIDY="${ION_BIN_TIDY:-}"
export ION_BIN_PARALLEL="${ION_BIN_PARALLEL:-parallel}"
export ION_BIN_STAT="${ION_BIN_STAT:-"stat"}"
export ION_BIN_FIND="${ION_BIN_FIND:-"bfs:find"}"
export ION_BIN_XARGS="${ION_BIN_XARGS:-xargs}"

export ION_BIN_STAT_BSD="${ION_BIN_STAT_BSD:-}"
export ION_BIN_STAT_GNU="${ION_BIN_STAT_GNU:-}"
export ION_BIN_FIND_GNU="${ION_BIN_FIND_GNU:-}"
export ION_BIN_XARGS_GNU="${ION_BIN_XARGS_GNU:-}"

export ION_DEV_URANDOM="${ION_DEV_URANDOM:-"/dev/urandom"}"

export ION_SERVE="${ION_SERVE:-2}"
export ION_SERVED="${ION_SERVED:-}"
export ION_SERVE_PRODUCTION="${ION_SERVE_PRODUCTION:-}"
export ION_SERVE_PORT="${ION_SERVE_PORT:-}"
export ION_SERVE_WWW="${ION_SERVE_WWW:-}"

export ION_WATCH="${ION_WATCH:-2}"
export ION_WATCH_CLEAR="${ION_WATCH_CLEAR:-1}"
export ION_WATCH_INITIAL="${ION_WATCH_INITIAL:-1}"
export ION_WATCH_THROTTLE="${ION_WATCH_THROTTLE:-0.1}"
export ION_WATCH_DEBOUNCE="${ION_WATCH_DEBOUNCE:-0.1}"

export ION_BUILD="${ION_BUILD:-}"
export ION_BUILD_CURRENT="${ION_BUILD_CURRENT:-}"
export ION_BUILD_PREVIOUS="${ION_BUILD_PREVIOUS:-}"
export ION_BUILD_LOG="${ION_BUILD_LOG:-}"
export ION_BUILD_LOG_KEEP="${ION_BUILD_LOG_KEEP:-}"
export ION_BUILD_JS="${ION_BUILD_JS:-1}"
export ION_BUILD_JS_GLOBAL="${ION_BUILD_JS_GLOBAL:-1}"
export ION_BUILD_CSS="${ION_BUILD_CSS:-1}"
export ION_BUILD_CSS_GLOBAL="${ION_BUILD_CSS_GLOBAL:-1}"
export ION_BUILD_HTML="${ION_BUILD_HTML:-1}"

export ION_INBOX="${ION_INBOX:-}"
export ION_INBOX_PORT="${ION_INBOX_PORT:-8000}"
export ION_INBOX_RATES="${ION_INBOX_RATES:-8192}"

export ION_LINK_PREFIX="${ION_LINK_PREFIX:-"."}"
export ION_LINK_PROTOCOL="${ION_LINK_PROTOCOL:-1}"
export ION_LINK_DOMAIN="${ION_LINK_DOMAIN:-1}"
export ION_LINK_TRIM="${ION_LINK_TRIM:-1}"

export ION_EXTRACT_MAXIMUM="${ION_EXTRACT_MAXIMUM:-160}"
export ION_EXTRACT_SUFFIX="${ION_EXTRACT_SUFFIX:-"…"}"

export ION_SOURCE="${ION_SOURCE:-}"
export ION_SOURCE_STYLES="${ION_SOURCE_STYLES:-}"
export ION_SOURCE_SCRIPTS="${ION_SOURCE_SCRIPTS:-}"

export ION_FILTER_PATH="${ION_FILTER_PATH:-}"
export ION_FILTER_OUTPUT="${ION_FILTER_OUTPUT:-}"
export ION_FILTER_TARGET="${ION_FILTER_TARGET:-}"

export ION_START_ID="${ION_START_ID:-0}"
export ION_START_CMD="${ION_START_CMD:-"env"}"
export ION_START_ARGS="${ION_START_ARGS:-}"

export ION_DOMAIN="${ION_DOMAIN:-"localhost"}"
export ION_SYNTAX="${ION_SYNTAX:-"monochrome"}"
export ION_COGNATES="${ION_COGNATES:-"inventori:inventory"}"

export ION_TEMP="${ION_TEMP:-"${TMPDIR:-/tmp}"}"
export ION_TEST="${ION_TEST:-1}"
export ION_WORDS="${ION_WORDS:-1}"

export ION_VOLUME="${ION_VOLUME:-3}"
export ION_MINIFY="${ION_MINIFY:-0}"
export ION_PARALLEL="${ION_PARALLEL:-0}"
export ION_CLUSTER="${ION_CLUSTER:-}"

export ION_INPUT="${ION_INPUT:-}"
export ION_MIRRORS="${ION_MIRRORS:-}"

TAB=
NEWLINE=
CARRIAGE=

SALT=
STARTED=
START_PID=

SERVER_PID=
SERVER_STARTED=
SERVED_TEMP=

WATCHER_PID_INPUT=
WATCHER_PID_SOURCE=

BUILD_TEMP=

TEMP_SED=
TEMP_BLANK=
TEMP_WATCH_LOCK=
TEMP_WATCH_STREAM=
TEMP_FILTER_EMPTY=
TEMP_FILTER_TEST=
TEMP_FILTER_SPLIT=
TEMP_FILTER_MERGE=
TEMP_FILTER_EXTRACT=
TEMP_FILTER_DOCUMENT=
TEMP_FILTER_TEMPLATE=
TEMP_TEMPLATE_JSON=
TEMP_TEMPLATE_HTML=
TEMP_SOURCE_STYLES=
TEMP_SOURCE_SCRIPTS=
TEMP_COMPILED=

SHARED_LUA="$(cat <<'EOF'
function env(name, default)
	local found = os.getenv("ION_"..name)
	found = found and #found > 0 and found or default
	assert(found, name.." not given")
	return found
end

function envn(name, default)
	return tonumber(env(name, default))
end

function envb(name, default)
	return envn(name, default) == 1
end

__ERROR_PREFIX_SUB = env("__ERROR_PREFIX_SUB")
__ERROR_PREFIX_SUBL = env("__ERROR_PREFIX_SUBL")
__ERROR_PREFIX_SUBR = env("__ERROR_PREFIX_SUBR")
__ERROR_INFIX_MAIN = env("__ERROR_INFIX_MAIN")
__ERROR_INFIX_SUB = env("__ERROR_INFIX_SUB")

__QUERY_PHRASE = env("__QUERY_PHRASE")
__QUERY_CLAUSE = env("__QUERY_CLAUSE")
__QUERY_SENTENCE = env("__QUERY_SENTENCE")
__QUERY_PARAGRAPH = env("__QUERY_PARAGRAPH")

__QUERY_SUBJECT = envn("__QUERY_SUBJECT", 1)
__QUERY_VERB = envn("__QUERY_VERB", 2)

__QUERY_ALL = env("__QUERY_ALL")
__QUERY_SEPARATOR = env("__QUERY_SEPARATOR")
__QUERY_NESTED_LEFT = env("__QUERY_NESTED_LEFT")
__QUERY_NESTED_RIGHT = env("__QUERY_NESTED_RIGHT")

__TITLE_SEPARATOR = env("__TITLE_SEPARATOR")
__TYPE_SEPARATOR = env("__TYPE_SEPARATOR")

_WORD_INDEX = env("_WORD_INDEX")
_WORD_ERROR = env("_WORD_ERROR")
_WORD_INFO = env("_WORD_INFO")
_WORD_NOTE = env("_WORD_NOTE")

_EXT_JSON = env("_EXT_JSON")
_EXT_HTML = env("_EXT_HTML")

_NAME_ROOT = env("_NAME_ROOT")
_NAME_BRANCH = env("_NAME_BRANCH")
_NAME_INDEX_JS = env("_NAME_INDEX_JS")
_NAME_INDEX_CSS = env("_NAME_INDEX_CSS")

_MSG_OPENING_FILE = env("_MSG_OPENING_FILE")
_MSG_QUERYING_THE_INDEX = env("_MSG_QUERYING_THE_INDEX")
_MSG_QUERY_FOUND_AN_ENTRY = env("_MSG_QUERY_FOUND_AN_ENTRY")

_VERB_IDENTITY = env("_VERB_IDENTITY")

_TYPE_NULL = env("_TYPE_NULL")
_TYPE_TRUE = env("_TYPE_TRUE")
_TYPE_FALSE = env("_TYPE_FALSE")
_TYPE_BOOLEAN = env("_TYPE_BOOLEAN")
_TYPE_NUMBER = env("_TYPE_NUMBER")
_TYPE_STRING = env("_TYPE_STRING")
_TYPE_TEXT = env("_TYPE_TEXT")
_TYPE_PATH = env("_TYPE_PATH")
_TYPE_PATHS = env("_TYPE_PATHS")
_TYPE_REFERENCE = env("_TYPE_REFERENCE")
_TYPE_RELATIONSHIP = env("_TYPE_RELATIONSHIP")
_TYPE_OBJECT = env("_TYPE_OBJECT")
_TYPE_ARRAY = env("_TYPE_ARRAY")

_META_TYPE = env("_META_TYPE")
_META_TYPE_DIRECTORY = env("_META_TYPE_DIRECTORY")
_META_TYPE_DOCUMENT = env("_META_TYPE_DOCUMENT")

_META_SCAN = env("_META_SCAN")

_META_PATH = env("_META_PATH")
_META_NAME = env("_META_NAME")
_META_EXTENSION = env("_META_EXTENSION")
_META_DOMAIN = env("_META_DOMAIN")

_META_HASH = env("_META_HASH")
_META_STAMP = env("_META_STAMP")
_META_SIGNER = env("_META_SIGNER")
_META_SIGNATURE = env("_META_SIGNATURE")
_META_SALT = env("_META_SALT")

_META_LOCATION = env("_META_LOCATION")
_META_ITERATION = env("_META_ITERATION")
_META_MODIFIED = env("_META_MODIFIED")
_META_SIZE = env("_META_SIZE")

_META_WIDTH = env("_META_WIDTH")
_META_HEIGHT = env("_META_HEIGHT")
_META_DEPTH = env("_META_DEPTH")
_META_LENGTH = env("_META_LENGTH")

_META_DIRECTION = env("_META_DIRECTION")
_META_ENCODING = env("_META_ENCODING")
_META_ALPHABET = env("_META_ALPHABET")
_META_LANGUAGE = env("_META_LANGUAGE")
_META_WORDMARK = env("_META_WORDMARK")
_META_FLAG = env("_META_FLAG")
_META_ICON = env("_META_ICON")

_META_TITLE = env("_META_TITLE")
_META_DESCRIPTION = env("_META_DESCRIPTION")
_META_PUBLISHED = env("_META_PUBLISHED")
_META_CATEGORY = env("_META_CATEGORY")
_META_LABELS = env("_META_LABELS")
_META_AUTHORS = env("_META_AUTHORS")
_META_ALBUM = env("_META_ALBUM")
_META_PUBLIC = env("_META_PUBLIC")
_META_COVER = env("_META_COVER")

_META_FROM = env("_META_FROM")
_META_PARENT = env("_META_PARENT")
_META_RELATED = env("_META_RELATED")
_META_PREVIOUSLY = env("_META_PREVIOUSLY")
_META_REFERENCES = env("_META_REFERENCES")
_META_DEPENDENCIES = env("_META_DEPENDENCIES")
_META_TRANSLATIONS = env("_META_TRANSLATIONS")
_META_DERIVATIVES = env("_META_DERIVATIVES")

_ATTR_CURRENT = env("_ATTR_CURRENT")
_ATTR_QUERY = env("_ATTR_QUERY")
_ATTR_MULTIPLE = env("_ATTR_MULTIPLE")
_ATTR_NESTED = env("_ATTR_NESTED")
_ATTR_KEY = env("_ATTR_KEY")
_ATTR_TYPE = env("_ATTR_TYPE")
_ATTR_VALUE = env("_ATTR_VALUE")
_ATTR_NAME = env("_ATTR_NAME")

_CLASS_INDEX = env("_CLASS_INDEX")
_CLASS_WORD = env("_CLASS_WORD")
_CLASS_PAGE = env("_CLASS_PAGE")
_CLASS_FORM = env("_CLASS_FORM")
_CLASS_HEADER = env("_CLASS_HEADER")
_CLASS_REQUIRED = env("_CLASS_REQUIRED")
_CLASS_COMPONENT = env("_CLASS_COMPONENT")
_CLASS_NO_JS = env("_CLASS_NO_JS")

START_ID = env("START_ID")

BIN_SELF = env("BIN_SELF")

LINK_PROTOCOL = envb("LINK_PROTOCOL")
LINK_DOMAIN = envb("LINK_DOMAIN")
LINK_PREFIX = env("LINK_PREFIX")
LINK_TRIM = envb("LINK_TRIM")

BUILD_JS = envb("BUILD_JS")
BUILD_CSS = envb("BUILD_CSS")

EXTRACT_SUFFIX = env("EXTRACT_SUFFIX", "")
EXTRACT_MAXIMUM = envn("EXTRACT_MAXIMUM", 0)

COGNATES = env("COGNATES")
TESTING = envb("TEST")
VOLUME = envn("VOLUME", 3)
WORDS = envb("WORDS")

FILTER_PATH = env("FILTER_PATH")
FILTER_TARGET = env("FILTER_TARGET")
FILTER_OUTPUT = env("FILTER_OUTPUT")

CACHED_COGNATES = nil
CACHED_INDEX = nil

REFERENCES = {}

Query = {}
Index = {}

function is_null(value)
	return type(value) == "nil"
end

function is_boolean(value)
	return type(value) == "boolean"
end

function is_number(value)
	return type(value) == "number"
end

function is_string(value)
	return type(value) == "string"
end

function is_function(value)
	return type(value) == "function"
end

function is_userdata(value)
	return type(value) == "userdata"
end

function is_table(value)
	local pandoc_type = pandoc.utils.type(value)
	return pandoc_type ~= "Inlines" and
	       pandoc_type ~= "Blocks" and
	       type(value) == "table"
end

function is_array(value)
	-- from: stackoverflow.com/a/25709704

	if pandoc.utils.type(value) == "List" then
		return #value > 0
	elseif is_table(value) and #value > 0 then
		local i = 0

		for _ in pairs(value) do
			i = i + 1
			if value[i] == nil then
				return false
			end
		end

		return true
	else
		return false
	end
end

function is_array__test()
	local empty = {}
	local array = {1,2,3}
	local object = {x=1, y=2, z=3}

	assert(not is_array(empty))
	assert(is_array(array))
	assert(not is_array(object))
end

function is_object(value)
	return is_table(value) and (next(value) ~= nil and not is_array(value))
end

function is_object__test()
	local empty = {}
	local array = {1, 2, 3}
	local object = {x=1, y=2, z=3}

	assert(not is_object(empty))
	assert(is_object(object))
	assert(not is_object(array))
end

function is_text(element)
	local pandoc_type = pandoc.utils.type(element)
	return pandoc_type == "Inlines" or
	       pandoc_type == "Inline" or
	       pandoc_type == "Blocks" or
	       pandoc_type == "Block" or
	       pandoc_type == "Meta"
end

function is_numbery(value)
	return value and is_number(value) and value ~= 0
end

function is_stringy(value)
	return value and is_string(value) and #value > 0
end

function is_tabley(value)
	return value and is_table(value) and next(value) ~= nil
end

function is_truthy(value)
	local truthy = not not value

	if truthy and is_number(value) then
		truthy = is_numbery(value)
	elseif truthy and is_string(value) then
		truthy = is_stringy(value)
	elseif truthy and is_table(value) then
		truthy = is_tabley(value)
	end

	return truthy
end

function json_decode(str, pandoc_types)
	return pandoc.json.decode(str, pandoc_types or false)
end

function json_encode(obj)
	return pandoc.json.encode(obj)
end

function json_encode_debug(value)
	if is_string(value) then
		return value
	elseif TESTING then
		return json_encode(value)
	else
		return tostring(value)
	end
end

function printe(label, ...)
	local args = table.pack(...)
	local started = false
	local err = ""

	local prefix = __ERROR_PREFIX_SUB..__ERROR_PREFIX_SUBL..START_ID..__ERROR_PREFIX_SUBR

	for i=1, args.n do
		local arg = args[i]

		if not started then
			err = prefix..label..__ERROR_INFIX_MAIN
			started = true
		else
			err = err..__ERROR_INFIX_SUB
		end

		err = err..json_encode_debug(arg)
	end

	if started then
		io.stderr:write(err.."\n")
		io.stderr:flush()
	end
end

function exit(...)
	if VOLUME > 0 then
		printe(_WORD_ERROR, ...)
	end

	os.exit(1)
end

function note(...)
	if VOLUME > 1 then
		printe(_WORD_NOTE, ...)
	end
end

function info(...)
	if VOLUME > 2 then
		printe(_WORD_INFO, ...)
	end
end

function table_copy(tbl)
	local tblm = getmetatable(tbl)
	local copy = {}

	for k, v in pairs(tbl) do
		if is_table(v) then
			copy[k] = table_copy(v)
		else
			copy[k] = v
		end
	end

	if tblm then
		setmetatable(copy, tblm)
	end

	return copy
end

function table_keys(tbl, comp)
	local keys = {}

	for k in pairs(tbl) do
		table.insert(keys, k)
	end

	table.sort(keys, comp)
	return keys
end

function table_keys__test()
	local object = {y=2, z=3, x=1}
	local keys = table_keys(object)

	assert(keys[1] == "x")
	assert(keys[2] == "y")
	assert(keys[3] == "z")
end

function table_values(tbl, comp, truthy)
	local keys = table_keys(tbl, comp)
	local values = {}

	for i, k in ipairs(keys) do
		local v = tbl[k]

		if not truthy or is_truthy(v) then
			table.insert(values, v)
		end
	end

	return values
end

function table_values__test()
	local object = {y=2, z=3, x=1}
	local values = table_values(object)

	assert(values[1] == 1)
	assert(values[2] == 2)
	assert(values[3] == 3)
end

function table_get_table(tbl, key)
	if is_table(tbl) and is_table(tbl[key]) then
		return tbl[key]
	else
		return {}
	end
end

function tables_get(key, ...)
	local tbls = table.pack(...)

	for i=1, tbls.n do
		local tbl = tbls[i]
		local value = tbl and tbl[key]

		if value then
			return value
		end
	end
end

function array_contains(array, target)
	local found = nil

	for i, v in ipairs(array) do
		if v == target then
			found = i
			break
		end
	end

	return found
end

function array_contains__test()
	local array = {1, 2, 3}

	assert(array_contains(array, 2))
	assert(not array_contains(array, 4))
	assert(not array_contains(array, "hello"))
end

function array_deduplicated(array)
	local fixed = {}

	for i, v in ipairs(array) do
		fixed[v] = true
	end

	return table_keys(fixed)
end

function array_deduplicated__test()
	local array = {3, 2, 1, 2}
	local fixed = array_deduplicated(array)

	assert(#fixed == 3)
	assert(fixed[1] == 1)
	assert(fixed[2] == 2)
	assert(fixed[3] == 3)
end

function string_trim(str)
	return str:gsub("^%s*(.-)%s*$", "%1")
end

function string_trim_lr(str, l, r)
	return str:gsub("^%s*"..l.."?%s*(.-)%s*"..(r or l).."?%s*$", "%1")
end

function string_find(str, pattern, plain)
	return str:find(pattern, 1, plain) ~= nil
end

function string_find_plain(str, pattern)
	return string_find(str, pattern, true)
end

function string_replace(str, pattern, repl, n)
	return str:gsub(pattern, repl, n)
end

function string_escape(s)
	-- see: lua.org/pil/20.2.html
	--                 ( ) . % + - * ? [   ^ $
    return s:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")
end

function string_split(str, pattern, f, strip, plain)
	str = strip and string_trim(str) or str

	local split = {}
	local i = 1

	repeat
		local s, e = str:find(pattern, i, plain)
		local value = str:sub(i, s and s-1 or nil)

		if strip then
			value = string_trim(value)
		end

		if f then
			value = f(value, #split+1)
		end

		if #value > 0 then
			table.insert(split, value)
		end

		i = e and e+1
	until not i or i > #str

	return split
end

function string_split_plain(str, pattern, f, strip)
	return string_split(str, pattern, f, strip, true)
end

function string_split_plainer(str, pattern, f)
	return string_split_plain(str, pattern, f, true)
end

function string_split__test()
	local unsplit_1 = ""
	local split_1 = string_split_plain(unsplit_1, ".")
	local stripped_1 = string_split_plainer(unsplit_1, ".")

	assert(#split_1 == 0)
	assert(#stripped_1 == 0)

	local unsplit_2 = " "
	local split_2 = string_split_plain(unsplit_2, ".")
	local stripped_2 = string_split_plainer(unsplit_2, ".")

	assert(#split_2 == 1)
	assert(#stripped_2 == 0)
	assert(split_2[1] == " ")

	local unsplit_3 = "  hello  "
	local split_3 = string_split_plain(unsplit_3, ".")
	local stripped_3 = string_split_plainer(unsplit_3, ".")

	assert(#split_3 == 1)
	assert(#stripped_3 == 1)
	assert(split_3[1] == "  hello  ")
	assert(stripped_3[1] == "hello")

	local unsplit_4 = "  hello  .  world  "
	local split_4 = string_split_plain(unsplit_4, ".")
	local stripped_4 = string_split_plainer(unsplit_4, ".")

	assert(#split_4 == 2)
	assert(#stripped_4 == 2)
	assert(split_4[1] == "  hello  ")
	assert(split_4[2] == "  world  ")
	assert(stripped_4[1] == "hello")
	assert(stripped_4[2] == "world")
end

function string_before(str, sep)
	local before = str:match("(.*)"..string_escape(sep))
	return before and #before > 0 and before or nil
end

function string_before__test()
	assert(string_before("/a/b", "/") == "/a")
	assert(string_before("/a/", "/") == "/a")
	assert(string_before("/a", "/") == nil)
	assert(string_before("/", "/") == nil)
	assert(string_before("a/b", "/") == "a")
	assert(string_before("a/", "/") == "a")
	assert(string_before("a", "/") == nil)
	assert(string_before("", "/") == nil)
end

function string_after(str, sep)
	local after = str:match(".*"..string_escape(sep).."(.*)")
	return after and #after > 0 and after or nil
end

function string_after__test()
	assert(string_after("/a/b", "/") == "b")
	assert(string_after("/a/", "/") == nil)
	assert(string_after("/a", "/") == "a")
	assert(string_after("/", "/") == nil)
	assert(string_after("a/b", "/") == "b")
	assert(string_after("a/", "/") == nil)
	assert(string_after("a", "/") == nil)
	assert(string_after("", "/") == nil)
end

function string_start(whole, part)
	return whole:sub(1, #part) == part
end

function string_end(whole, part)
	return whole:sub(-#part) == part
end

function extension_is_document(extension)
	local extensions = {
		txt = true,
		md = true,
		markdown = true,
		asciidoc = true,
		textile = true,
		adoc = true,
		rst = true,
		t2t = true,
		dbk = true,
		tex = true,
		ms = true,
		odt = true,
		fodt = true,
		docx = true,
		rtf = true
	}

	return extensions[extension] ~= nil
end

function extension_mime(extension)
	local mimes = {
		gif = "image/gif",
		png = "image/png",
		svg = "image/svg+xml",
		ico = "image/x-icon"
	}

	return mimes[extension]
end

function extension_trim(extension)
	return string_trim_lr(extension, "%.")
end

function path_is_safe(str)
	local safe = str ~= nil
	safe = safe and str ~= ""
	safe = safe and not str:match(
        "[\0\t\n\r\\*?$`\"'<>%(%)%[%]{}|&;:%%~]")
	return safe
end

function path_is_absolute(normal)
	return normal:sub(1, 1) == "/"
end

function path_is_relative(normal)
	return not path_is_absolute(normal)
end

function path_normal(path)
	local collapsed = path:gsub("/+", "/")
	local stripped = collapsed:gsub("^%./", "")
	return stripped:gsub("/$", "")
end

function path_parent(path)
	local normal = path_normal(path)
	return string_before(normal, "/")
end

function path_file(path)
	local normal = path_normal(path)
	return string_after(normal, "/") or normal
end

function path_file_parts(path)
	local file = path_file(path)
	return string_split_plain(file, ".")
end

function path_name(path)
	local parts = path_file_parts(path)
	return parts[1] or path
end

function path_ext_get(path)
	local parts = path_file_parts(path)
	table.remove(parts, 1)

	return table.concat(parts, ".")
end

function path_ext_set(path, extension)
	local parent = path_parent(path)
	local name = path_name(path)
	local base = parent or ""

	if name then
		if #base > 0 then
			base = base.."/"..name
		else
			base = name
		end

		if extension then
			base = base.."."..extension
		end
	end

	if path_is_absolute(path) and not path_is_absolute(base) then
		base = "/"..base
	end

	return base
end

function path_ext(path, extension)
	if extension ~= nil then
		return path_ext_set(path, extension)
	else
		return path_ext_get(path)
	end
end

function path_trim(path)
	return path:gsub("^/*(.-)/*$", "%1")
end

function path_trim_start(path)
	return path:gsub("^/*(.*)", "%1")
end

function path_trim_end(path)
	return path:gsub("/+$", "")
end

function path_split(normal)
	return string_split_plain(path_trim(normal), "/")
end

function path_join(...)
	local paths = table.pack(...)
	local joined = ""

	for i=1, paths.n do
		if paths[i] then
			if #joined > 0 then
				joined = path_trim_end(joined).."/"..path_trim_start(paths[i])
			else
				joined = paths[i]
			end
		end
	end

	return joined
end

function path_absolute(path)
	return path_is_absolute(path) and path or "/"..path
end

function path_resolve(normal)
	local is_absolute = path_is_absolute(normal)
	local unresolved = path_split(normal)
	local resolved = {}

	while #unresolved > 0 do
		local current = table.remove(unresolved, 1)

		if current == ".." then
			table.remove(resolved)
		elseif current ~= "." then
			table.insert(resolved, current)
		end
	end

	local path = path_join(table.unpack(resolved))
	return is_absolute and "/"..path or path
end

function path_resolve__test()
	assert(path_resolve("a") == "a")
	assert(path_resolve("/a") == "/a")
	assert(path_resolve("./a") == "a")
	assert(path_resolve("../a") == "a")
	assert(path_resolve("/a/./b/") == "/a/b")
	assert(path_resolve("/a/../b") == "/b")
	assert(path_resolve("/a/./../b/") == "/b")
	assert(path_resolve("/a/b/..") == "/a")
	assert(path_resolve("/a/b/.") == "/a/b")
	assert(path_resolve("/a/b/") == "/a/b")
end

function path_rooted(given)
	local normal = path_normal(given)

	if path_is_relative(normal) then
		local parent = FILTER_PATH and path_parent(FILTER_PATH)

		if parent then
			normal = parent.."/"..normal
		else
			normal = "/"..normal
		end
	end

	return path_resolve(normal)
end

function path_is_document(path)
	return extension_is_document(path_ext(path))
end

function file_open(path, write, binary)
	info(_MSG_OPENING_FILE, path)
	local mode = (write and "w" or "r")..(binary and "b" or "")
	return assert(io.open(path, mode), _MSG_OPENING_FILE)
end

function file_close(file)
	return file:close()
end

function file_flush(file)
	return file:flush()
end

function file_seek(file, ...)
	return file:seek(...)
end

function file_reset(file)
	return file_seek(file, "set", 0)
end

function file_read(file, amount)
	if is_string(file) then
		local f = file_open(file)
		local content = file_read(f, amount)
		file_close(f)
		return content
	else
		return file:read(amount or "a")
	end
end

function file_decode(file, amount)
	return json_decode(file_read(file, amount))
end

function file_lines(file)
	file_reset(file)
	return file:lines()
end

function file_write(file, ...)
	if is_string(file) then
		local f = file_open(file, true)
		local r = file_write(f, ...)
		file_close(f)
		return r
	else
		return file:write(...)
	end
end

function file_copy(src, dst)
	-- see: forum.cockos.com/showthread.php?t=244397

	local src_file = file_open(src, false, true)
	local dst_file = file_open(dst, true, true)
	local src_size, dst_size = 0, 0

	while true do
		local block = file_read(src_file, 2^13)

		if not block then
			src_size = file_seek(src_file, "end")
			break
		end

		file_write(dst_file, block)
	end

	dst_size = file_seek(dst_file, "end")

	file_close(src_file)
	file_close(dst_file)

	return src_size == dst_size
end

function location_is_all(location)
	return location == __QUERY_ALL
end

function location_is_self(location)
	return not location or #location == 0 or location == FILTER_PATH
end

function location_is_multiple(location)
	return string_end(location, "/")
end

function location_is_location(location)
	return string_start(location, "/")
end

function location_normal(location)
	local self = location_is_self(location)
	return self and path_rooted(FILTER_PATH) or path_rooted(location)
end

function link_split_anchor(link)
	local split = string_split_plain(link, "#")
	return split[1], is_stringy(split[2]) and split[2]
end

function link_join_anchor(path, anchor)
	return path.."#"..anchor
end

function link_is_anchor(link)
	return string_start(link, "#")
end

function link_is_external(link)
	return string_find(link, "^[%w_-]+:")
end

function link_trim_start(link)
	return link:gsub("^/+", "")
end

function link_trim_end(link)
	return link:gsub("/+$", "")
end

function link_join(a, b, double)
	local atrim = a and link_trim_end(a)
	local btrim = b and link_trim_start(b)

	if is_stringy(atrim) and is_stringy(btrim) then
		return atrim..(double and "//" or "/")..btrim
	elseif is_stringy(atrim) then
		return atrim
	else
		return btrim
	end
end

function link_fix(link)
	local domain = meta(_META_DOMAIN)

	if is_stringy(LINK_PREFIX) then
		link = link_join(LINK_PREFIX, link)
	end

	if is_stringy(domain) and LINK_DOMAIN then
		link = link_join(domain, link)
	end

	if LINK_PROTOCOL and FILTER_TARGET == _EXT_HTML then
		link = link_join("http:", link, true)
	end

	if LINK_TRIM then
		if path_is_document(link) then
			link = path_ext(link, false)
		end
	end

	return link
end

function link_normal(link, reference, fix)
	local is_external = link_is_external(link)
	local is_anchor = link_is_anchor(link)
	local is_current = false
	local anchor = nil

	if is_anchor then
		return link, true, false, is_anchor
	end

	link, anchor = link_split_anchor(link)

	if not is_external then
		link = path_rooted(link)
		is_current = link == FILTER_PATH
	end

	if reference ~= false then
		table.insert(REFERENCES, link)
	end

	if fix ~= false then
		link = link_fix(link)
	end

	if anchor then
		link = link_join_anchor(link, anchor)
	end

	return link, is_current, is_external, is_anchor
end

function subtype_join(...)
	return table.concat(table.pack(...), __TYPE_SEPARATOR)
end

function subtype_split(subtype)
	return subtype and string_split_plain(subtype, __TYPE_SEPARATOR) or {}
end

function subtype_is_boolean(subtype)
	return subtype == _TYPE_BOOLEAN
end

function subtype_is_number(subtype)
	return subtype == _TYPE_NUMBER
end

function subtype_is_string(subtype)
	return subtype == _TYPE_STRING or
	       subtype == _TYPE_PATH or
	       subtype == _TYPE_PATHS or
	       subtype == _TYPE_REFERENCE or
	       subtype == _TYPE_RELATIONSHIP
end

function subtype_is_path(subtype)
	return subtype == _TYPE_PATH or
	       subtype == _TYPE_REFERENCE or
	       subtype == _TYPE_RELATIONSHIP
end

function subtype_is_paths(subtype)
	return subtype == _TYPE_PATHS
end

function subtype_is_reference(subtype)
	return subtype == _TYPE_REFERENCE
end

function subtype_is_relationship(subtype)
	return subtype == _TYPE_RELATIONSHIP
end

function subtype_is_object(subtype)
	return subtype == _TYPE_OBJECT
end

function subtype_is_array(subtype)
	return subtype == _TYPE_ARRAY
end

function subtype_is_collection(subtype)
	return subtype_is_object(subtype) or
	       subtype_is_array(subtype)
end

function subtype_object(subtype)
	return subtype_join(_TYPE_OBJECT, subtype)
end

function subtype_array(subtype)
	return subtype_join(_TYPE_ARRAY, subtype)
end

function subtype_plural(subtype)
	local subtypes = subtype_split(subtype)

	if #subtypes > 1 then
		table.remove(subtypes)
		return table.concat(subtypes, __TYPE_SEPARATOR)
	elseif subtypes[1] and subtype_is_collection(subtypes[1]) then
		return subtypes[1]
	end
end

function subtype_single(subtype)
	local subtypes = subtype_split(subtype)

	if #subtypes > 1 then
		return subtypes[#subtypes]
	elseif subtypes[1] and not subtype_is_collection(subtypes[1]) then
		return subtypes[1]
	end
end

function decode_array(str, f)
	local lines = {}

	if str then
		for s in str:gmatch("[^\r\n]+") do
			if f then
				s = f(s)
			end

			table.insert(lines, s)
		end
	end

	return lines
end

function decode_paths_array(lines)
	return decode_array(lines, function(line)
		return string_split_plainer(line, ":")
	end)
end

function decode_cognates()
	local cognates = decode_paths_array(COGNATES)
	local decoded = {}

	for _, cognate in ipairs(cognates) do
		local k = cognate[1]
		local v = cognate[2]

		if k and v then
			decoded[k] = v
		end
	end
	
	return decoded
end

function translate(word)
	if not CACHED_COGNATES then
		CACHED_COGNATES = decode_cognates()
	end

	return CACHED_COGNATES[word] or word
end

function merge(a, b, _)
	return b or a
end

function merge__test()
	assert(merge(2, 1, _TYPE_NUMBER) == 1)
end

function phrase_normal(phrase, i)
	local map_f = (i == __QUERY_VERB or i == __QUERY_SUBJECT) and translate
	return string_split_plainer(phrase, __QUERY_PHRASE, map_f)
end

function phrase_find(phrase, entry)
	for i, word in ipairs(phrase) do
		entry = is_table(entry) and entry[word] or nil

		if not entry then
			break
		end
	end

	return entry
end

function verb_list()
	return {
		[_VERB_IDENTITY] = function(value)
			return value
		end
	}
end

function verb_find(verb)
	return phrase_find(verb or {_VERB_IDENTITY}, verb_list())
end

function verb_apply(verb, subject, objects, entry)
	local agent = phrase_find(subject, entry)
	local f = verb_find(verb)

	if agent and f then
		return f(agent, objects, entry)
	end
end

function clause_normal(clause)
	local objects = string_split_plainer(clause, __QUERY_CLAUSE, phrase_normal)
	local subject = nil
	local verb = nil

	if __QUERY_VERB > __QUERY_SUBJECT then
		subject = table.remove(objects, __QUERY_SUBJECT)
		verb = table.remove(objects, __QUERY_VERB-1)
	else
		verb = table.remove(objects, __QUERY_VERB)
		subject = table.remove(objects, __QUERY_SUBJECT-1)
	end

	table.insert(objects, 1, subject)
	table.insert(objects, 1, verb)

	return objects
end

function clause_evaluate(clause, entry)
	local objects = table_copy(clause)
	local verb = table.remove(objects, 1)
	local subject = table.remove(objects, 1)
	return verb_apply(verb, subject, objects, entry)
end

function sentence_normal(sentence)
	return string_split_plainer(sentence, __QUERY_SENTENCE, clause_normal)
end

function sentence_evaluate(sentence, entry)
	local result = nil

	for i, clause in ipairs(sentence) do
		result = clause_evaluate(clause, entry)

		if not result then
			return false
		end
	end

	return result
end

function paragraph_normal(conditions)
	return string_split_plainer(conditions, __QUERY_PARAGRAPH, sentence_normal)
end

function paragraph_evaluate(conditions, entry)
	local result = #conditions == 0 and entry or nil

	for i, sentence in ipairs(conditions) do
		result = sentence_evaluate(sentence, entry)

		if result then
			break
		end
	end

	return result
end

function convert_to_span(content, attributes)
	return pandoc.Span(content, attributes)
end

function convert_to_link(content, attributes, key)
	local reference = true
	local title = nil
	local fix = true

	local target, current = link_normal(key, reference, fix)

	if attributes then
		attributes[_ATTR_NAME] = path_name(target)
		attributes[_ATTR_CURRENT] = current and "" or nil
	end

	return pandoc.Link(content, target, title, attributes)
end

function convert_single(key, value, link)
	local attributes = {}
	local content = {}
	local data = nil
	local kind = ""

	if is_array(value) then
		kind = _TYPE_ARRAY
		for i, v in ipairs(value) do
			table.insert(content, convert_single(i, v, false))
		end
	elseif is_table(value) then
		kind = _TYPE_OBJECT
		for k, v in pairs(value) do
			table.insert(content, convert_single(k, v, false))
		end
	elseif is_string(value) then
		kind = _TYPE_STRING
		data = value
		content = ""
	elseif is_number(value) then
		kind = _TYPE_NUMBER
		data = tostring(value)
		content = ""
	elseif is_boolean(value) then
		kind = _TYPE_BOOLEAN
		data = value and _TYPE_TRUE or _TYPE_FALSE
		content = ""
	elseif is_null(value) then
		kind = _TYPE_NULL
		data = ""
		content = ""
	end

	attributes[_ATTR_KEY] = key or nil
	attributes[_ATTR_TYPE] = kind
	attributes[_ATTR_VALUE] = data

	if link then
		return convert_to_link(content, attributes, key)
	else
		return convert_to_span(content, attributes)
	end
end

function convert_to_nest(results)
	local nested = {}

	for path in pairs(results) do
		local parent = path_parent(path)
		local ancestry = parent and path_split(parent) or {}
		local current = nested

		for i, name in ipairs(ancestry) do
			if not current[name] then
				current[name] = {}
			end

			current = current[name]
		end

		table.insert(current, path)
	end

	return nested
end

function convert_from_nest(results, attributes, nested, parent)
	if is_table(nested) then
		local names = table_keys(nested)
		local converted = {}

		for i, name in ipairs(names) do
			local value = convert_from_nest(results, nil, nested[name], name)
			table.insert(converted, value)
		end

		attributes = attributes or {}
		attributes[_ATTR_TYPE] = _META_TYPE_DIRECTORY
		attributes[_ATTR_NAME] = parent or ""

		return convert_to_span(converted, attributes)
	elseif is_string(nested) then
		return results[nested]
	end
end

function convert_nested(results, attributes)
	local nested = convert_to_nest(results)
	return convert_from_nest(results, attributes, nested)
end

function convert_flat(results, attributes)
	local values = table_values(results)
	return convert_to_span(values, attributes)
end

function convert_multiple(results, attributes, nest)
	if nest then
		return convert_nested(results, attributes)
	else
		return convert_flat(results, attributes)
	end
end

function Query.is_nested(query)
	return string_find(query, "^%s*"..__QUERY_NESTED_LEFT.."%s*") and
	       string_find(query, "%s*"..__QUERY_NESTED_RIGHT.."%s*$")
end

function Query.trim(query, nested)
	if nested then
		return string_trim_lr(query, __QUERY_NESTED_LEFT, __QUERY_NESTED_RIGHT)
	else
		return string_trim(query)
	end
end

function Query.is_split(query)
	return string_find_plain(query, __QUERY_SEPARATOR)
end

function Query.split(query)
	local split = string_split(query, "%s*"..__QUERY_SEPARATOR.."+%s*")
	return split[1], split[2]
end

function Query.from_table(options)
	options = setmetatable(options or {}, { __index = Query })

	options.raw = options.raw or ""
	options.kind = options.kind or nil
	options.path = location_normal(options.path)
	options.conditions = options.conditions or {}

	if options.multiple == nil then
		options.multiple = location_is_multiple(options.path)
	end

	if options.nested == nil then
		options.nested = false
	end

	if options.convert == nil then
		options.convert = false
	end

	if options.reference == nil then
		options.reference = true
	end

	return options
end

function Query.from_string(str, options)
	local nested = Query.is_nested(str)
	local query = Query.trim(str, nested)
	local path, conditions = "", ""

	if Query.is_split(query) then
		path, conditions = Query.split(query)
	elseif location_is_location(query) then
		path = query
	else
		conditions = query
	end

	options = options or {}
	options.path = options.path or path
	options.conditions = options.conditions or paragraph_normal(conditions)
	options.nested = options.nested or nested
	options.raw = options.raw or str

	return Query.from_table(options)
end

function Query.new(query, options)
	if is_string(query) then
		return Query.from_string(query, options)
	elseif is_table(query) then
		return Query.from_table(query)
	else
		return Query.from_table(options)
	end
end

function Query:input_stream(lines)
	return function()
		local line = lines()

		if line then
			local entry = json_decode(line)
			local path = entry and entry[_META_PATH]
			return entry, path
		end
	end
end

function Query:input_table(input)
	input = self.multiple and input or { [self.path] = input[self.path] }

	local paths = table_keys(input)
	local paths_it = ipairs(paths)
	local i = 0

	return function()
		local _, path = paths_it(paths, i)
		local entry = input[path]
		i = i + 1

		if path and entry then
			entry[_META_PATH] = entry[_META_PATH] or path
			return entry, path
		end
	end
end

function Query:input(input)
	input = input or {}

	if is_function(input) then
		return input
	elseif is_table(input) then
		if getmetatable(input) == Index then
			return input:all()
		else
			return self:input_table(input)
		end
	elseif input then
		return self:input_stream(file_lines(input))
	end
end

function Query:output_table(output, entry, path)
	if self.convert then
		if self.reference then
			table.insert(REFERENCES, path)
		end

		entry = convert_single(path, entry, true)
	elseif output[path] then
		entry = merge(output[path], entry, _TYPE_OBJECT)
	end

	output[path] = entry
end

function Query:output_stream(output, entry, path)
	file_write(output, json_encode(entry), "\n")
end

function Query:output(output, entry, path)
	info(_MSG_QUERY_FOUND_AN_ENTRY, path, entry)

	if is_table(output) then
		self:output_table(output, entry, path)
	elseif output then
		self:output_stream(output, entry, path)
	end
end

function Query:finish_table_single(output, attributes)
	local single = output[self.path]

	if self.convert then
		single = single or convert_to_span("")

		for k, v in pairs(attributes) do
			single.attributes[k] = v
		end
	end

	return single
end

function Query:finish_table_multiple(output, attributes)
	if self.convert then
		attributes[_ATTR_MULTIPLE] = ""
		attributes[_ATTR_NESTED] = self.nested and ""
		return convert_multiple(output, attributes, self.nested)
	else
		return output
	end
end

function Query:finish_table(output)
	local attributes = {}
	attributes[_ATTR_QUERY] = self.raw

	if self.multiple then
		return self:finish_table_multiple(output, attributes)
	else
		return self:finish_table_single(output, attributes)
	end
end

function Query:finish_stream(output)
	file_flush(output)
	return output
end

function Query:finish(output)
	if is_table(output) then
		return self:finish_table(output)
	elseif output then
		return self:finish_stream(output)
	end
end

function Query:match_path(entry, path)
	if self.multiple then
		return string_start(path, self.path)
	else
		return path == self.path
	end
end

function Query:match_kind(entry, path)
	return not self.kind or self.kind == entry[_META_TYPE]
end

function Query:match_evaluation(entry, path)
	return paragraph_evaluate(self.conditions, entry)
end

function Query:match(entry, path)
	return self:match_path(entry, path)
	   and self:match_kind(entry, path)
	   and self:match_evaluation(entry, path)
end

function Query:filter(input, output)
	info(_MSG_QUERYING_THE_INDEX, self.raw)

	input = self:input(input)
	output = output or {}

	for entry, path in input do
		local found = self:match(entry, path)

		if found then
			self:output(output, found, path)
	
			if path == self.path and not self.multiple then
				break
			end
		end
	end

	return self:finish(output)
end

function query(q, input, output, options)
	return Query.new(q, options):filter(input, output)
end

function query__test()

end

function Index.part(part)
	if part:find("^[^/,]*$") and path_is_safe(part) and #part <= 64 then
		return part:gsub("%.+", ".")
	end
end

function Index.name(key, kind)
	key = Index.part(key)
	kind = Index.part(kind)

	if is_stringy(key) and is_stringy(kind) then
		return key..","..kind
	end
end

function Index:path(path, key, kind)
	local root = path_join(self.output, _NAME_ROOT)
	local name = Index.name(key, kind)
	return name and path_join(root, path, name)
end

function Index:file_internal(write, binary, ...)
	local path = self:path(...)
	return path and file_open(path, write, binary)
end

function Index:file(...)
	return self:file_internal(false, false, ...)
end

function Index.open(output)
	local self = setmetatable({}, { __index = Index })

	self.output = output
	self.scan_file = output and self:file(_NAME_BRANCH, _META_SCAN, subtype_array(_TYPE_PATHS))

	return self.scan_file and self
end

function Index:close()
	if self.scan_file then
		file_close(self.scan_file)
		self.scan_file = nil
	end
end

function Index:scan_lines(f)
	self.scan_iterator = self.scan_file and file_lines(self.scan_file)

	return function()
		if self.scan_iterator then
			local line = self.scan_iterator()

			if f and line then
				return f(line)
			else
				return line
			end
		end
	end
end

function Index:scan_columns(f)
	return self:scan_lines(function(line)
		local columns = string_split_plainer(line, ":")

		if f and columns then
			return f(columns)
		else
			return columns
		end
	end)
end

function Index:scan_paths(f)
	return self:scan_columns(function(columns)
		local path = columns[1]

		if f and path then
			return f(path)
		else
			return path
		end
	end)
end

function Index:scan_entries(f)
	return self:scan_paths(function(path)
		local file_path = path_join(self.output, path)
		local json_path = path_ext(file_path, _EXT_JSON)
		local decoded = json_path and file_decode(json_path)

		if f and decoded then
			return f(decoded, path)
		else
			return decoded, path
		end
	end)
end

function Index:all()
	return self:scan_entries()
end

function index_close()
	if CACHED_INDEX then
		CACHED_INDEX:close()
	end
end

function index_open(output)
	if CACHED_INDEX then
		index_close()
	end

	CACHED_INDEX = Index.open(output or FILTER_OUTPUT)
end

function meta(q, options)
	local input = CACHED_INDEX and CACHED_INDEX:all()
	local output = nil

	if is_stringy(options) then
		options = { path = options }
	else
		options = options or { path = FILTER_PATH }
	end

	return query(q, input, output, options)
end

function extract_suffix()
	return is_stringy(EXTRACT_SUFFIX) and
	       translate(EXTRACT_SUFFIX) or ""
end

function extract_length()
	local suffix = extract_suffix()
	local maximum = EXTRACT_MAXIMUM - #suffix
	return maximum > 0 and maximum or nil
end

function extract_copy(tree, maximum)
	local finished = false
	local spaced = true
	local extract = {}
	local count = 0

	local append_nothing = function(el)
		return {}, false
	end

	local append_space = function()
		if not finished and not spaced then
			table.insert(extract, pandoc.Space())
			count = count + 1
			spaced = true
		end

		return nil, not finished
	end

	local append_word = function(text)
		if not finished and maximum and (count + #text) > maximum then
			finished = true
		end

		if not finished then
			table.insert(extract, pandoc.Str(text))
			count = count + #text
			spaced = false
		end

		return nil, not finished
	end

	local append_text = function(el)
		return append_word(el.text)
	end

	tree:walk({
		traverse = "topdown",

		Str = append_text,
		Code = append_text,
		Space = append_space,
		Block = append_space,
		LineBreak = append_space,
		SoftBreak = append_space,
		CodeBlock = append_nothing,
		Header = append_nothing,
		Figure = append_nothing,
		Image = append_nothing,
		Table = append_nothing,
		Div = append_nothing
	})

	if extract[#extract] == pandoc.Space() then
		extract[#extract] = nil
	end

	if finished then
		table.insert(extract, pandoc.Str(extract_suffix()))
	end

	return #extract > 0 and extract or nil
end

function extract_string(tree, maximum)
	if is_text(tree) then
		local copy = extract_copy(tree, maximum)
		return copy and pandoc.utils.stringify(copy) or ""
	elseif tree then
		local str = tostring(tree)

		if maximum and str and #str > maximum then
			str = str:sub(1, maximum)
			str = string_trim(str)..extract_suffix()
		end

		return str
	else
		return ""
	end
end

function extract_header(tree)
	local header = nil

	tree:walk({
		traverse = "topdown",

		Header = function(el)
			if not header then
				header = extract_copy(el.content)
			end

			return nil, header == nil
		end
	})

	return header
end

function extract_wordcount(tree)
	-- see: github.com/pandoc/lua-filters/blob/master/wordcount/wordcount.lua

	local count = 0

	local count_str = function(el)
		if #el.text > 0 and el.text:match("%P") then
			count = count + 1
		end
	end

	local count_code = function(el)
		local _, n = el.text:gsub("%S+", "")
		count = count + n
	end

	tree:walk({
		Str = count_str,
		Code = count_code,
		CodeBlock = count_code
	})

	return count
end

function extract_title()
	local prefix, suffix = "", ""
	local name = meta(_META_WORDMARK)
	local title = meta(_META_TITLE) or meta(_META_NAME)
	local direction = meta(_META_DIRECTION)

	if title and name and __TITLE_SEPARATOR then
		if direction == "rtl" then
			prefix = name..__TITLE_SEPARATOR
		else
			suffix = __TITLE_SEPARATOR..name
		end
	end

	return prefix..extract_string(title)..suffix
end

function extract_authors()
	local authors = meta(_META_AUTHORS)
	local names = {}

	if is_table(authors) then
		for i, path in ipairs(authors) do
			table.insert(names, meta(_META_TITLE, path))
		end
	end

	return #names > 0 and names or nil
end

function extract_icon()
	local icon = meta(_META_ICON)
	local icon_extension = icon and path_ext(icon)
	local icon_type = extension_mime(icon_extension)
	return icon, icon_type
end

function extract_defaults(tree)
	if tree.meta[_META_NAME] == nil then
		tree.meta[_META_NAME] = path_name(FILTER_PATH)
	end

	if tree.meta[_META_TITLE] == nil then
		tree.meta[_META_TITLE] = extract_header(tree.blocks)
	end

	if tree.meta[_META_TITLE] == nil then
		tree.meta[_META_TITLE] = tree.meta[_META_NAME]
	end

	if tree.meta[_META_DESCRIPTION] == nil then
		tree.meta[_META_DESCRIPTION] = extract_copy(tree.blocks, extract_length())
	end

	if tree.meta[_META_LENGTH] == nil then
		tree.meta[_META_LENGTH] = extract_wordcount(tree.blocks)
	end

	return tree
end

function extract_references(tree)
	return tree
end

function filter_divs(tree)
	local function input_name(text)
		local trimmed = string_trim(text)
		local cleaned = string_replace(trimmed, "[^%w%s-_]+", "_")
		local dashed = string_replace(cleaned, "%s+", "-")
		return pandoc.text.lower(dashed)
	end

	local function inputify(span)
		local text = extract_string(span.content)
		local name = input_name(text)

		span.attributes["name"] = name
		span.attributes["placeholder"] = text
		span.attributes["type"] = span.classes[1]
		span.classes[1] = nil

		if array_contains(span.classes, _CLASS_REQUIRED) then
			span.attributes["required"] = true
		end

		return span
	end

	return tree:walk({
		Div = function(div)
			if array_contains(div.classes, _CLASS_FORM) then
				return div:walk({ Span = inputify })
			else
				return div
			end
		end
	})
end

function filter_spans(tree)
	return tree:walk({
		Span = function(element)
			if array_contains(element.classes, "mark") then
				return meta(pandoc.utils.stringify(element.content), {
					kind = _META_TYPE_DOCUMENT,
					convert = true
				})
			else
				return element
			end
		end
	})
end

function filter_links(tree)
	return tree:walk({
		Link = function(element)
			local target, current, external = link_normal(element.target, true, true)
			element.attributes[_ATTR_CURRENT] = current and "" or nil
			element.attributes.target = external and "_blank" or "_self"
			element.target = target
			return element
		end
	})
end

function filter_images(tree)
	return tree:walk({
		Image = function(element)
			element.src = link_normal(element.src, true, true)
			return element
		end
	})
end

function filter_classes(tree)
	local filter_element = function(element)
		local classes = {}

		for i, v in ipairs(element.classes) do
			local t = translate(v)
			table.insert(classes, v)

			if t ~= v then
				table.insert(classes, t)
			end
		end

		element.classes = classes
		return element
	end

	return tree:walk({
		Div = filter_element,
		Span = filter_element,
		Table = filter_element,
		TableHead = filter_element,
		TableFoot = filter_element,
		CodeBlock = filter_element,
		Figure = filter_element,
		Header = filter_element,
		Image = filter_element,
		Link = filter_element,
		Code = filter_element,
		Cell = filter_element
	})
end

function filter_headers(tree)
	return tree:walk({
		Header = function(element)
			table.insert(element.classes, _CLASS_HEADER)
			return element
		end
	})
end

function filter_words(tree)
	if WORDS then
		return tree:walk({
			Str = function(element)
				return convert_to_span(element.text, {class=_CLASS_WORD})
			end
		})
	else
		return tree
	end
end

function filter_page(tree)
	local blocks = {
		meta(__QUERY_ALL, {convert=true, nested=true}),
		pandoc.Div(tree.blocks, {class=_CLASS_PAGE})
	}

	return pandoc.Pandoc(blocks, tree.meta)
end

function template_defaults(tree)
	local icon, icon_type = extract_icon()

	if not tree.meta["template-link-js"] and BUILD_JS then
		tree.meta["template-link-js"] = link_normal(_NAME_INDEX_JS)
	end

	if not tree.meta["template-link-css"] and BUILD_CSS then
		tree.meta["template-link-css"] = link_normal(_NAME_INDEX_CSS)
	end

	if not tree.meta["template-class-no-js"] and BUILD_JS then
		tree.meta["template-class-no-js"] = _CLASS_NO_JS
	end

	if not tree.meta["template-class-component"] then
		tree.meta["template-class-component"] = _CLASS_COMPONENT
	end

	if not tree.meta["template-icon"] then
		tree.meta["template-icon"] = icon
	end

	if not tree.meta["template-icon-type"] then
		tree.meta["template-icon-type"] = icon_type
	end

	if not tree.meta["template-encoding"] then
		tree.meta["template-encoding"] = meta(_META_ENCODING)
	end

	if not tree.meta["template-language"] then
		tree.meta["template-language"] = meta(_META_LANGUAGE)
	end

	if not tree.meta["template-direction"] then
		tree.meta["template-direction"] = meta(_META_DIRECTION)
	end

	if not tree.meta["template-title"] then
		tree.meta["template-title"] = extract_title()
	end

	if not tree.meta["template-description"] then
		tree.meta["template-description"] = meta(_META_DESCRIPTION)
	end

	if not tree.meta["template-authors"] then
		tree.meta["template-authors"] = extract_authors()
	end

	if not tree.meta["template-keywords"] then
		tree.meta["template-keywords"] = meta(_META_LABELS)
	end

	if not tree.meta["template-no-js"] then
		tree.meta["template-no-js"] = _CLASS_NO_JS
	end

	if not tree.meta["template-generator"] then
		tree.meta["template-generator"] = path_name(BIN_SELF)
	end

	return tree
end

function test_all()
	is_array__test()
	is_object__test()
	table_keys__test()
	table_values__test()
	array_contains__test()
	array_deduplicated__test()
	string_split__test()
	string_before__test()
	string_after__test()
	path_resolve__test()
	query__test()
	merge__test()
end
EOF
)"

FILTER_EMPTY="$(cat <<'EOF'
Pandoc = function(tree)
	return tree
end
EOF
)"

FILTER_TEST="$(cat <<'EOF'
Pandoc = function(tree)
	test_all()
end
EOF
)"

FILTER_SPLIT="$(cat <<'EOF'
Pandoc = function(tree)
	index_open()
	query(__QUERY_ALL, file_decode(io.stdin), io.stdout)
	index_close()
	os.exit(0)
end
EOF
)"

FILTER_MERGE="$(cat <<'EOF'
Pandoc = function(tree)
	index_open()
	tree.meta = query(__QUERY_ALL, io.stdin)
	index_close()
	return tree
end
EOF
)"

FILTER_EXTRACT="$(cat <<'EOF'
Pandoc = function(tree)
	tree = extract_defaults(tree)
	tree = extract_references(tree)
	return tree
end
EOF
)"

FILTER_DOCUMENT="$(cat <<'EOF'
Pandoc = function(tree)
	index_open()

	tree = filter_divs(tree)
	tree = filter_spans(tree)
	tree = filter_links(tree)
	tree = filter_images(tree)
	tree = filter_page(tree)
	tree = filter_words(tree)
	tree = filter_headers(tree)
	tree = filter_classes(tree)

	index_close()
	return tree
end
EOF
)"

FILTER_TEMPLATE="$(cat <<'EOF'
Pandoc = function(tree)
	index_open()
	tree = template_defaults(tree)
	index_close()
	return tree
end
EOF
)"

GLOBAL_CSS="$(cat <<EOF

EOF
)"

GLOBAL_JS_ENV="$(cat <<EOF

EOF
)"

GLOBAL_JS="$(cat <<'EOF'
	var COMPONENTS = {};

	function document_ready(node, f) {
		if (node.readyState === "loading") {
			node.addEventListener("DOMContentLoaded", f);
		} else {
			f();
		}
	}

	function component_add(name, f) {
		if (!Array.isArray(COMPONENTS[name])) {
			COMPONENTS[name] = [];
		}
		
		COMPONENTS[name].push(f);
	}

	function components_run() {
		
	}

	function components_start() {
		document_ready(document, components_run);
	}
EOF
)"

TEMPLATE_HTML="$(cat <<'EOF'
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"$if(template-language)$ lang="$template-language$" xml:lang="$template-language$"$endif$$if(template-direction)$ dir="$template-direction$"$endif$ class="$template-no-js$">
<head>
	<meta charset="$template-encoding$" />
	<meta name="generator" content="$template-generator$" />
	<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes" />

	$if(template-date)$
		<meta name="date" content="$template-date$" />
	$endif$

	$for(template-authors)$
		<meta name="author" content="$template-authors$" />
	$endfor$

	$if(template-keywords)$
		<meta name="keywords" content="$for(template-keywords)$$template-keywords$$sep$, $endfor$" />
	$endif$

	$if(template-description)$
		<meta name="description" content="$template-description$" />
	$endif$

	$if(template-icon)$
		<link rel="icon"$if(template-icon-type)$ type="$template-icon-type$"$endif$ href="$template-icon$">
	$endif$

	$if(template-title)$
		<title>$template-title$</title>
	$endif$

	<style>
		:root:not(.$template-class-no-js$) :not(.$template-class-component$),
		:root:not(.$template-class-no-js$) :not(.$template-class-component$)::before,
		:root:not(.$template-class-no-js$) :not(.$template-class-component$)::after {
			pointer-events: none;
			user-select: none;
			opacity: 0;
		}
	</style>

	<script>
		var JS = typeof Symbol === "function";
		
		if (JS) {
			document.documentElement.classList.remove("$template-class-no-js$");
		}
	</script>

	$if(template-link-js)$
		<script src="$template-link-js$" defer></script>
	$endif$

	$if(template-link-css)$
		<link rel="stylesheet" href="$template-link-css$">
	$endif$
</head>
<body>
$body$
</body>
</html>
EOF
)"

TEMPLATE_CADDY_DEVELOPMENT="$(cat <<'EOF'
PROTOCOL://DOMAIN_NAME:SERVE_PORT {
EOF
)"

TEMPLATE_CADDY_PRODUCTION="$(cat <<'EOF'
PROTOCOL://DOMAIN_NAME {
EOF
)"

TEMPLATE_CADDY_SHARED="$(cat <<'EOF'
	@post method POST
	reverse_proxy @post localhost:INBOX_PORT

	root * "ROOT_DIRECTORY"
	file_server
}
EOF
)"

TEMPLATE_CADDY_WWW="$(cat <<'EOF'
PROTOCOL://DOMAIN_NAME {
	redir PROTOCOL://www.{host}{uri}
}
EOF
)"

TEMPLATE_CADDY_UNWWW="$(cat <<'EOF'
PROTOCOL://www.DOMAIN_NAME {
	redir PROTOCOL://DOMAIN_NAME{uri}
}
EOF
)"

set -f #set -euf

is_bool() {
	! test "$1" || test "$1" = 1
}

is_uint() {
	# see: stackoverflow.com/a/61835747

	case $1 in
		'' | *[!0-9]*) return 1;;
		*) return 0 ;;
	esac
}

is_int() {
	case ${1#[-+]} in
		'' | *[!0-9]*) return 1;;
		*) return 0 ;;
	esac
}

is_unum() {
	case $1 in
		'' | . | *[!0-9.]* | *.*.*) return 1;;
		*) return 0 ;;
	esac
}

is_num() {
	case ${1#[-+]} in
		'' | . | *[!0-9.]* | *.*.*) return 1;;
		*) return 0 ;;
	esac
}

terminal_clear() {
	# see: stackoverflow.com/a/37778152/22451530
	# and: student.cs.uwaterloo.ca/~cs452/terminal.html
	test "$ION_WATCH_CLEAR" && test -t 1 && printf '\033[2J\033[H'
}

find_command() {
	command -v "$@" 2>/dev/null || true
}

found_command() {
	test "$(find_command "$@")"
}

found_os() {
	test "$(uname -s)" = "$1"
}

found_posix() {
	test -w /dev/null && test -w "$ION_TEMP" && found_command find
}

have_parent() {
	test "$ION_START_ID" != 0
}

have_flock() {
	test "$ION_BIN_FLOCK"
}

have_caddy() {
	test "$ION_BIN_CADDY"
}

have_fswatch() {
	test "$ION_BIN_FSWATCH"
}

have_esbuild() {
	test "$ION_BIN_ESBUILD"
}

have_ln() {
	test "$ION_BIN_LN"
}

have_luac() {
	test "$ION_BIN_LUAC"
}

have_openssl() {
	test "$ION_BIN_OPENSSL"
}

have_pandoc() {
	test "$ION_BIN_PANDOC"
}

have_rclone() {
	test "$ION_BIN_RCLONE"
}

have_sha256sum() {
	test "$ION_BIN_SHA256SUM"
}

have_sha256() {
	test "$ION_BIN_SHA256"
}

have_shasum() {
	test "$ION_BIN_SHASUM"
}

have_shellcheck() {
	test "$ION_BIN_SHELLCHECK"
}

have_ssh() {
	test "$ION_BIN_SSH"
}

have_tcpserver() {
	test "$ION_BIN_TCPSERVER"
}

have_tidy() {
	test "$ION_BIN_TIDY"
}

have_urandom() {
	test -r "$ION_DEV_URANDOM"
}

have_random() {
	have_urandom || have_openssl
}

have_hash() {
	have_sha256sum || have_sha256 || have_shasum || have_openssl
}

have_stat() {
	test "$ION_BIN_STAT" 
}

have_stat_gnu() {
	test "$ION_BIN_STAT_GNU" = 1
}

have_stat_bsd() {
	test "$ION_BIN_STAT_BSD" = 1
}

have_stat_recognised() {
	have_stat && { have_stat_gnu || have_stat_bsd; }
}

have_find_gnu() {
	test "$ION_BIN_FIND_GNU" = 1
}

have_find() {
	test "$ION_BIN_FIND" && { have_find_gnu || have_stat_recognised; }
}

have_xargs() {
	test "$ION_BIN_XARGS"
}

have_xargs_gnu() {
	test "$ION_BIN_XARGS_GNU" = 1
}

have_parallel() {
	test "$ION_BIN_PARALLEL"
}

have_watcher() {
	have_fswatch
}

have_server_back() {
	have_tcpserver
}

have_server_front() {
	have_caddy
}

have_server() {
	have_server_front && have_server_back
}

building_html() {
	test "$ION_BUILD_HTML" = 1
}

should_help() {
	! test "$ION_INPUT" && ! have_parent
}

should_test() {
	! should_help && ! have_parent && test "$ION_TEST" = 1
}

should_build() {
	test "$ION_BUILD_CURRENT"
}

should_watch_initial() {
	! should_help && test "$ION_WATCH_INITIAL" = 1
}

should_watch() {
	! should_help && ! have_parent && {
		test "$ION_WATCH" = 1 || {
			test "$ION_WATCH" = 2 && have_watcher
		}
	}
}

should_serve() {
	! should_help && ! have_parent && {
		test "$ION_SERVE" = 1 || {
			test "$ION_SERVE" = 2 && have_server
		}
	}
}

can_build() {
	have_xargs || have_xargs_gnu || have_parallel
}

can_watch() {
	test "$ION_WATCH" = 0 || test "$ION_WATCH" = 2 || {
		test "$ION_WATCH" = 1 && have_watcher
	}
}

can_serve() {
	test "$ION_SERVE" = 0 || test "$ION_SERVE" = 2 || {
		test "$ION_SERVE" = 1 && have_server
	}
}

print() {
	printf '%s\n' "$@"
}

printd() {
	printf '%d\n' "$@"
}

printe() {
	ab__label="$1"; shift
	ab__started=0
	ab__err=

	if have_parent; then
		ab__prefix="$ION___ERROR_PREFIX_SUB$ION___ERROR_PREFIX_SUBL$ION_START_ID$ION___ERROR_PREFIX_SUBR"
	else
		ab__prefix="$ION___ERROR_PREFIX_MAIN"
	fi

	for ab__part in "$@"; do
		if test "$ab__started" -eq 0; then
			ab__err="$ab__prefix$ab__label$ION___ERROR_INFIX_MAIN"
			ab__started=1
		else
			ab__err="$ab__err$ION___ERROR_INFIX_SUB"
		fi

		ab__err="$ab__err$ab__part"
	done

	if test "$ab__started" -eq 1; then
		>&2 printf '%s\n' "$ab__err"
	fi
}

error() {
	if test "$ION_VOLUME" -gt 0; then
		printe "$ION__WORD_ERROR" "$@"
	fi

	return 1
}

note() {
	if test "$ION_VOLUME" -gt 1; then
		printe "$ION__WORD_NOTE" "$@"
	fi
}

info() {
	if test "$ION_VOLUME" -gt 2; then
		printe "$ION__WORD_INFO" "$@"
	fi
}

floor() {
	di__minimum=${2:-1}
	di__floored=0

	if is_num "$1"; then
		di__floored="${1%%.*}"
	fi

	if test "$di__floored" -lt "$di__minimum"; then
		print "$di__minimum"
	else
		print "$di__floored"
	fi
}

timestamp() {
	# from: unix.stackexchange.com/a/703287

	print $(($(TZ=GMT0 date \
	+"((%Y-1600)*365+(%Y-1600)/4-(%Y-1600)/100+(%Y-1600)/400+1%j-1000-135140)\
	*86400+(1%H-100)*3600+(1%M-100)*60+(1%S-100)")))
}

string_trim_internal() {
	# see: dylanaraps/pure-sh-bible#trim-all-white-space-from-string-and-truncate-spaces
	# see: en.wikibooks.org/wiki/Regular_Expressions/POSIX_Basic_Regular_Expressions
	bq__trimmed="${2#"${2%%[![:"$1":]]*}"}"
	print "${bq__trimmed%"${bq__trimmed##*[![:"$1":]]}"}"
}

string_trim_graph() {
	string_trim_internal graph "$@"
}

string_trim_punct() {
	string_trim_internal punct "$@"
}

string_trim() {
	string_trim_internal space "$@"
}

string_split() {
	# see: dylanaraps/pure-sh-bible#split-a-string-on-a-delimiter

	ac__old_ifs="$IFS"
	IFS="$2"

	# shellcheck disable=SC2086
	set -- $1

	IFS="$ac__old_ifs"
	printf '%s\n' "$@"
}

string_quoted() {
	# from: gist.github.com/miguelmota/a0d37934c689f9ad000d01306905868c
	print "$1" | sed -e s/\"/'\\\"'/g
}

string_has_newline() {
	case "$1" in
		*"$NEWLINE"*) return 0 ;;
		*) return 1 ;;
	esac
}

string_escape() {
	# from: stackoverflow.com/a/2705678/22451530
	# and: stackoverflow.com/q/29613304
	print "$1" | sed -e 's/[\/&]/\\&/g'
}

string_replace() {
	if ! string_has_newline "$2"; then
		sed -e 's/'"$(string_escape "$1")"'/'"$(string_escape "$2")"'/g'
	else
		error "$ION__MSG_INVALID_REPLACEMENT" "$@" || return
	fi
}

string_replace_with() {
	# from: unix.stackexchange.com/a/49438/583801
	sed -e '/'"$1"'/ {' -e 'r '"$2" -e 'd' -e '}'
}

ext_data() {
	case "$1" in
		csv) return 0 ;;
		tsv) return 0 ;;
		json) return 0 ;;
		yaml) return 0 ;;
		yml) return 0 ;;
		xml) return 0 ;;
		xlsx) return 0 ;;
		*) return 1 ;;
	esac
}

ext_document() {
	case "$1" in
		adoc) return 0 ;;
		asciidoc) return 0 ;;
		docx) return 0 ;;
		fb2) return 0 ;;
		htm) return 0 ;;
		html) return 0 ;;
		ipynb) return 0 ;;
		latex) return 0 ;;
		markdown) return 0 ;;
		md) return 0 ;;
		odt) return 0 ;;
		org) return 0 ;;
		pptx) return 0 ;;
		rst) return 0 ;;
		rtf) return 0 ;;
		tex) return 0 ;;
		textile) return 0 ;;
		txt) return 0 ;;
		*) return 1 ;;
	esac
}

ext_code() {
	case "$1" in
		sh) return 0 ;;
		*) return 1 ;;
	esac
}

ext_style() {
	case "$1" in
		css) return 0 ;;
		*) return 1 ;;
	esac
}

ext_script() {
	case "$1" in
		js) return 0 ;;
		jsx) return 0 ;;
		ts) return 0 ;;
		tsx) return 0 ;;
		*) return 1 ;;
	esac
}

ext_markdown() {
	case "$1" in
		markdown) return 0 ;;
		md) return 0 ;;
		*) return 1 ;;
	esac
}

ext_bitmap() {
	case "$1" in
		png) return 0 ;;
		gif) return 0 ;;
		jpg) return 0 ;;
		jpeg) return 0 ;;
		tiff) return 0 ;;
		webp) return 0 ;;
		avif) return 0 ;;
		heif) return 0 ;;
		*) return 1 ;;
	esac
}

ext_vector() {
	case "$1" in
		svg) return 0 ;;
		*) return 1 ;;
	esac
}

ext_image() {
	ext_bitmap "$1" || ext_vector "$1"
}

ext_audio() {
	case "$1" in
		mp3) return 0 ;;
		ogg) return 0 ;;
		oga) return 0 ;;
		opus) return 0 ;;
		spx) return 0 ;;
		flac) return 0 ;;
		wav) return 0 ;;
		wave) return 0 ;;
		*) return 1 ;;
	esac
}

ext_video() {
	case "$1" in
		ogv) return 0 ;;
		webm) return 0 ;;
		mp4) return 0 ;;
		*) return 1 ;;
	esac
}

ext_map() {
	case "$1" in
		osm.pbf) return 0 ;;
		osm.bz2) return 0 ;;
		*) return 1 ;;
	esac
}

ext_object() {
	case "$1" in
		obj) return 0 ;;
		gltf) return 0 ;;
		glb) return 0 ;;
		*) return 1 ;;
	esac
}

ext_font() {
	case "$1" in
		ttf) return 0 ;;
		otf) return 0 ;;
		woff) return 0 ;;
		woff2) return 0 ;;
		*) return 1 ;;
	esac
}

string_is_safe() {
	case "$1" in
		*"$NEWLINE"*) return 1 ;;
		*"$CARRIAGE"*) return 1 ;;
		*) return 0 ;;
	esac
}

name_is_safe() {
	case "$1" in
		*/*) return 1 ;;
		*) return 0 ;;
	esac
}

path_is_safe() {
	case "$1" in
		"") return 1 ;;
		*"$TAB"*) return 1 ;;
		*"$NEWLINE"*) return 1 ;;
		*"$CARRIAGE"*) return 1 ;;
		*\\*) return 1 ;;
		*\**) return 1 ;;
		*\?*) return 1 ;;
		*\$*) return 1 ;;
		*\`*) return 1 ;;
		*\"*) return 1 ;;
		*\'*) return 1 ;;
		*\<*) return 1 ;;
		*\>*) return 1 ;;
		*\(*) return 1 ;;
		*\)*) return 1 ;;
		*\[*) return 1 ;;
		*\]*) return 1 ;;
		*\{*) return 1 ;;
		*\}*) return 1 ;;
		*\|*) return 1 ;;
		*\&*) return 1 ;;
		*\;*) return 1 ;;
		*:*) return 1 ;;
		*%*) return 1 ;;
		*~*) return 1 ;;
		*) return 0 ;;
	esac
}

path_is_exec() {
	test -x "$1"
}

path_is_file() {
	test -f "$1"
}

path_is_dir() {
	test -d "$1"
}

path_is_single() {
	test "${1##*/*}"
}

path_is_absolute() {
	case "$1" in
		/*) return 0 ;;
		 *) return 1 ;;
	esac
}

path_is_name() {
	test "$1" && case "$1" in
		*/*) return 1 ;;
		  *) return 0 ;;
	esac
}

path_trim_parent() {
	print "${1##*/}"
}

path_trim_name() {
	print "${1%/*}"
}

path_trim_start() {
	print "${1##"${1%%[!/]*}"}"
}

path_trim_end() {
	print "${1%%"${1##*[!/]}"}"
}

path_trim() {
	path_trim_end "$(path_trim_start "$1")"
}

path_parent() {
	# from: dylanaraps/pure-sh-bible#get-the-directory-name-of-a-file-path
	# dirname() { path_parent "$1" "." "/" }

	af__path="$1"
	af__relative="${2:-}"
	af__absolute="${3:-}"

	af__parent="${af__path:-"$af__relative"}"
	af__parent="$(path_trim_end "$af__parent")" || return

	if path_is_single "$af__parent"; then
		af__parent="$af__relative"
	fi

	af__parent="$(path_trim_name "$af__parent")" || return
	af__parent="$(path_trim_end "$af__parent")" || return

	print "${af__parent:-"$af__absolute"}"
}

path_file() {
	# from: dylanaraps/pure-sh-bible#get-the-base-name-of-a-file-path
	ag__name="$(path_trim_end "$1")" || return
	ag__name="$(path_trim_parent "$ag__name")" || return
	print "$ag__name"
}

path_name() {
	au__file="$(path_file "$1")" || return
	print "${au__file%%.*}"
}

path_ext_get() {
	av__file="$(path_file "$1")" || return

	case "$av__file" in
		*.*) print "${av__file#*.}" ;;
		  *) print "" ;;
	esac
}

path_ext_set() {
	aw__path="$1"
	aw__extension="$2"

	aw__base="$(path_parent "$aw__path")" || return
	aw__name="$(path_name "$aw__path")" || return

	if test "$aw__name"; then
		if test "$aw__base"; then
			aw__base="$aw__base/$aw__name"
		else
			aw__base="$aw__name"
		fi

		if test "$aw__extension" != "."; then
			aw__base="$aw__base.$aw__extension" || return
		fi
	fi

	if path_is_absolute "$aw__path" && ! path_is_absolute "$aw__base"; then
		aw__base="/$aw__base"
	fi

	print "$aw__base"
}

path_type() {
	eq__indicator="$1"
	eq__extension="$2"

	eq__character="$(printf '%s' "$eq__indicator" | cut -c1-1)" || return

	if test "$eq__character" = "d"; then
		printf '%s' "$ION__META_TYPE_DIRECTORY"
	elif ext_data "$eq__extension"; then
		printf '%s' "$ION__META_TYPE_DATA"
	elif ext_document "$eq__extension"; then
		printf '%s' "$ION__META_TYPE_DOCUMENT"
	elif ext_code "$eq__extension"; then
		printf '%s' "$ION__META_TYPE_CODE"
	elif ext_style "$eq__extension"; then
		printf '%s' "$ION__META_TYPE_STYLE"
	elif ext_script "$eq__extension"; then
		printf '%s' "$ION__META_TYPE_SCRIPT"
	elif ext_image "$eq__extension"; then
		printf '%s' "$ION__META_TYPE_IMAGE"
	elif ext_audio "$eq__extension"; then
		printf '%s' "$ION__META_TYPE_AUDIO"
	elif ext_video "$eq__extension"; then
		printf '%s' "$ION__META_TYPE_VIDEO"
	elif ext_map "$eq__extension"; then
		printf '%s' "$ION__META_TYPE_MAP"
	elif ext_object "$eq__extension"; then
		printf '%s' "$ION__META_TYPE_OBJECT"
	elif ext_font "$eq__extension"; then
		printf '%s' "$ION__META_TYPE_FONT"
	else
		printf '%s' "$ION__META_TYPE_FILE"
	fi
}

path_absolute_from() {
	ct__cwd="$(pwd)" || return
	
	if test -d "$1"; then
		dir_change "$1" || return
		ct__path="$(pwd)" || return
		dir_change "$ct__cwd" || return
		print "$ct__path"
	fi
}

path_absolute() {
	# see: stackoverflow.com/a/23002317
	# and: stackoverflow.com/a/51264222

	ah__absolute=""

	if path_is_absolute "$1"; then
		ah__absolute="$1"
	elif path_is_dir "$1"; then
		ah__absolute="$(path_absolute_from "$1")" || return
	elif path_is_name "$1"; then
		ah__absolute="$(pwd)/$1" || return
	else
		ah__name="$(path_trim_parent "$1")" || return
		ah__parent="$(path_trim_name "$1")" || return
		ah__absparent="$(path_absolute_from "$ah__parent")" || return
		ah__absolute="$ah__absparent/$ah__name" || return
	fi

	print "$ah__absolute"
}

path_normal() {
	ai__absolute="$(path_absolute "$1")" || return
	ai__trimmed="$(path_trim_end "$ai__absolute")" || return
	print "${ai__trimmed:-"/"}"
}

path_join() {
	aj__joined=""

	for aj__part in "$@"; do
		if test "$aj__joined"; then
			aj__trimmed="$(path_trim "$aj__part")" || return
			aj__joined="$aj__joined/$aj__trimmed"
		else
			aj__trimmed="$(path_trim_end "$aj__part")" || return
			aj__joined="$aj__trimmed"
		fi
	done

	print "$aj__joined"
}

paths_split_raw() {
	string_split "$1" ":"
}

paths_split() {
	paths_split_raw "$@" | while IFS= read -r cw__path; do
		if test "$cw__path"; then
			cw__normal="$(path_normal "$cw__path")" || return
	
			if path_is_safe "$cw__normal"; then
				print "$cw__normal"
			fi
		fi
	done
}

paths_join() {
	if test "$1" && test "$2"; then
		printf '%s:%s' "$1" "$2"
	elif test "$1"; then
		printf '%s' "$1"
	else
		printf '%s' "$2"
	fi
}

paths_normal() {
	paths_split "$@" | {
		cy__paths=""

		while IFS= read -r cy__path; do
			if test "$cy__paths" && test "$cy__path"; then
				cy__paths="$cy__paths:$cy__path"
			else
				cy__paths="$cy__path"
			fi
		done
	
		print "$cy__paths"
	}
}

file_make() {
	touch -- "$1" || error "$ION__MSG_MAKING_FILE" "$1"
}

file_remove() {
	! test "$1" || rm -f -- "$1" || error "$ION__MSG_REMOVING_FILE" "$1"
}

file_move() {
	mv -- "$1" "$2" || error "$ION__MSG_MOVING_FILE" "$1" "$2"
}

file_copy() {
	co__result=1

	if have_ln; then
		"$ION_BIN_LN" -f -- "$@" 2>/dev/null || co__result=$?

		if test "$co__result" -ne 0; then
			note "$ION__MSG_LINKING_FILE" "$@"
			export ION_BIN_LN=
		fi
	fi

	if test "$co__result" -ne 0; then
		cp -f -- "$@" || return
	fi
}

dir_change() {
	cd -- "$1" || error "$ION__MSG_CHANGING_DIR" "$1"
}

dir_make() {
	mkdir -- "$1" || error "$ION__MSG_MAKING_DIR" "$1"
}

dir_make_all() {
	mkdir -p -- "$1" || error "$ION__MSG_MAKING_DIR" "$1"
}

dir_empty() {
	# see: unix.stackexchange.com/a/77313
	eo__path="$(path_normal "$1")" || return

	if test -d "$eo__path" && test "$eo__path" != "/"; then
		set +f
		rm -rf -- \
			"${eo__path:?}"/* \
			"${eo__path:?}"/.[!.]* \
			"${eo__path:?}"/..?* \
		2>/dev/null || true
		set -f
	fi
}

dir_remove() {
	df__path="$(path_normal "$1")" || return
	df__ret=0

	if test -d "$df__path" && test "$df__path" != "/"; then
		rm -rf -- "$df__path" 2>/dev/null || df__ret=$?
	fi

	return "$df__ret"
}

stop() {
	if test "$1"; then
		kill -TERM "$1" 2>/dev/null || true
		wait "$1" 2>/dev/null || true
	fi
}

start() {
	eh__ret=0
	eh__ifs="$IFS"
	eh__start_id="$ION_START_ID"
	IFS=" "

	export ION_START_ID=1

	# shellcheck disable=SC2086
	info "$ION__MSG_RUNNING_COMMAND" "$ION_START_CMD" $ION_START_ARGS "$@"

	# shellcheck disable=SC2086
	"$ION_START_CMD" $ION_START_ARGS "$@" || eh__ret=$?

	export ION_START_ID="$eh__start_id"
	IFS="$eh__ifs"

	return "$eh__ret"
}

# shellcheck disable=SC2120
start_bg() {
	em__ifs="$IFS"
	em__start_id="$ION_START_ID"
	IFS=" "

	export ION_START_ID=1

	# shellcheck disable=SC2086
	info "$ION__MSG_RUNNING_COMMAND_BG" "$ION_START_CMD" $ION_START_ARGS "$@"

	# shellcheck disable=SC2086
	"$ION_START_CMD" $ION_START_ARGS "$@" &

	START_PID=$!
	IFS="$em__ifs"
	export ION_START_ID="$em__start_id"
}

start_many() {
	ev__start_id="$ION_START_ID"
	ev__old_volume="$ION_VOLUME"
	ev__volume="$ION_VOLUME"
	ev__ifs="$IFS"
	ev__args=
	ev__ret=0
	IFS=" "

	if test "$ev__volume" -gt 3; then
		ev__args="-t"
	fi

	if have_parallel; then
		ev__cmd="$ION_BIN_PARALLEL"
		ev__args="$ev__args --line-buffer"

		if test "$ev__volume" -gt 1 && test -t 2; then
			ev__args="$ev__args --progress"
		fi

		if test "$ION_PARALLEL"; then
			ev__args="$ev__args -j$ION_PARALLEL"
		fi
	elif have_xargs_gnu; then
		ev__cmd="$ION_BIN_XARGS"
		ev__args="$ev__args -d""$NEWLINE"

		if test "$ION_PARALLEL"; then
			ev__args="$ev__args -P$ION_PARALLEL"
			ev__volume=0
		fi
	elif have_xargs; then
		ev__cmd="$ION_BIN_XARGS"
		ev__args="$ev__args -L1"
	fi

	export ION_START_ID=1
	export ION_VOLUME="$ev__volume"

	# shellcheck disable=SC2086
	info "$ION__MSG_RUNNING_COMMAND_MANY" "$ION_START_CMD" $ION_START_ARGS "$@"

	# shellcheck disable=SC2086
	"$ev__cmd" $ev__args "$ION_START_CMD" $ION_START_ARGS "$@" || ev__ret=$?

	export ION_START_ID="$ev__start_id"
	export ION_VOLUME="$ev__old_volume"

	IFS="$ev__ifs"
	return "$ev__ret"
}

start_stat() {
	if have_stat_gnu; then
		"$ION_BIN_STAT" -c "$1" "$3"
	elif have_stat_bsd; then
		"$ION_BIN_STAT" -f "$2" "$3"
	fi
}

start_stat_time() {
	start_stat '%Y' '%m' "$1"
}

start_stat_size() {
	start_stat '%s' '%z' "$1"
}

start_size_ls() {
	# from: stackoverflow.com/a/40167066
	dk__output="$(ls -dn -- "$1")" || return

	dk__ifs="$IFS"
	IFS=" "

	# shellcheck disable=SC2086
	set -- $dk__output && printd "$5"

	IFS="$dk__ifs"
}

start_size() {
	if have_stat_recognised; then
		start_stat_size "$1"
	else
		start_size_ls "$1"
	fi
}

start_find() {
	# see: stackoverflow.com/q/9612090/22451530
	# and: stackoverflow.com/q/21726862/22451530
	# and: unix.stackexchange.com/a/298595

	cn__dir="$1"
	cn__flat="$2"
	cn__extra="$3"

	cn__args=
	cn__dirs_l=
	cn__dirs_r=
	cn__dir_l=
	cn__dir_r=

	if test "$cn__flat"; then
		cn__dirs_l="$cn__dirs_l""!"
		cn__dirs_l="$cn__dirs_l""$NEWLINE""-name"
		cn__dirs_l="$cn__dirs_l""$NEWLINE""."
		cn__dirs_l="$cn__dirs_l""$NEWLINE""-prune"
		cn__dir_l="!"
	else
		cn__dirs_r="$cn__dirs_r""-prune"
		cn__dirs_r="$cn__dirs_r""$NEWLINE""-o"
		cn__dir_r="-o"
	fi

	if ! test "$cn__extra"; then
		cn__args="$cn__args""-print"
	elif have_find_gnu; then
		cn__args="$cn__args""-printf"
		cn__args="$cn__args""$NEWLINE""%p:%s:%Ts:%M\n"
	elif have_stat_gnu; then
		cn__args="$cn__args""-exec"
		cn__args="$cn__args""$NEWLINE""$ION_BIN_STAT"
		cn__args="$cn__args""$NEWLINE""-c"
		cn__args="$cn__args""$NEWLINE""%n:%s:%Y:%A"
		cn__args="$cn__args""$NEWLINE""{}"
		cn__args="$cn__args""$NEWLINE""+"
	elif have_stat_bsd; then
		cn__args="$cn__args""-exec"
		cn__args="$cn__args""$NEWLINE""$ION_BIN_STAT"
		cn__args="$cn__args""$NEWLINE""-f"
		cn__args="$cn__args""$NEWLINE""%N:%z:%m:%Sp"
		cn__args="$cn__args""$NEWLINE""{}"
		cn__args="$cn__args""$NEWLINE""+"
	else
		error "$ION__MSG_COMMAND_NOT_FOUND" "find" || return
	fi

	(
		cd -- "$cn__dir" || exit

		IFS="$NEWLINE"

		# shellcheck disable=SC2086
		"$ION_BIN_FIND" . $cn__dirs_l \( \
			$cn__dir_l -name '.git' $cn__dir_r \
			$cn__dir_l -name '.hg' $cn__dir_r \
			$cn__dir_l -path './'"$ION__NAME_ROOT" \
		\) $cn__dirs_r \( \
			-type f -o \
			-type d \
		\) \
			! -name "$ION__NAME_BRANCH" \
			! -name '*'"$TAB"'*' \
			! -name '*'"$NEWLINE"'*' \
			! -name '*'"$CARRIAGE"'*' \
			! -name '*\\*' \
			! -name '*\**' \
			! -name '*\?*' \
			! -name '*$*' \
			! -name '*`*' \
			! -name '*"*' \
			! -name "*'*" \
			! -name '*<*' \
			! -name '*>*' \
			! -name '*(*' \
			! -name '*)*' \
			! -name '*\[*' \
			! -name '*]*' \
			! -name '*{*' \
			! -name '*}*' \
			! -name '*|*' \
			! -name '*&*' \
			! -name '*;*' \
			! -name '*:*' \
			! -name '*%*' \
			! -name '*~*' \
			! -name '.*' \
			$cn__args
	)
}

start_random_urandom() {
	# from: unix.stackexchange.com/a/230676/583801
	LC_ALL=C tr -dc a-zA-Z0-9 <"$ION_DEV_URANDOM" 2>/dev/null | dd bs="$1" count=1 2>/dev/null || return
}

start_random_openssl() {
	start "$ION_BIN_OPENSSL" rand -base64 "$1" | tr "+/=" "xyz" | cut -c 1-"$1"
}

start_random() {
	am__amount="$1"

	if have_urandom; then
		start_random_urandom "$am__amount" || return
	elif have_openssl; then
		start_random_openssl "$am__amount" || return
	else
		return 1
	fi
}

start_hash_sha256_sha256sum() {
	an__output="$(start "$ION_BIN_SHA256SUM" < "$1")" || return

	an__ifs="$IFS"
	IFS=" "

	# shellcheck disable=SC2086
	set -- $an__output && print "$1"

	IFS="$an__ifs"
}

start_hash_sha256_sha256() {
	start "$ION_BIN_SHA256" -q "$1"
}

start_hash_sha256_shasum() {
	ao__output="$(start "$ION_BIN_SHASUM" -a 256 < "$1")" || return

	ao__ifs="$IFS"
	IFS=" "

	# shellcheck disable=SC2086
	set -- $ao__output && print "$1"

	IFS="$ao__ifs"
}

start_hash_sha256_openssl() {
	ap__output="$(start "$ION_BIN_OPENSSL" dgst -sha256 < "$1")" || return

	ap__ifs="$IFS"
	IFS=" "

	# shellcheck disable=SC2086
	set -- $ap__output && print "$2"

	IFS="$ap__ifs"
}

start_hash() {
	aq__digest=""

	if have_sha256sum; then
		aq__digest="$(start_hash_sha256_sha256sum "$1")" || return 1
	elif have_sha256; then
		aq__digest="$(start_hash_sha256_sha256 "$1")" || return 1
	elif have_shasum; then
		aq__digest="$(start_hash_sha256_shasum "$1")" || return 1
	elif have_openssl; then
		aq__digest="$(start_hash_sha256_openssl "$1")" || return 1
	fi

	if test "$aq__digest"; then
		print "sha256-$aq__digest"
		return 0
	else
		return 1
	fi
}

start_temp_template() {
	ar__prefix="$1"
	ar__extension="$2"
	ar__directory="$ION_TEMP"

	if ! test "$SALT"; then
		SALT="$(start_random 16)" || return
	fi

	if path_is_dir "$ar__directory"; then
		ar__directory="$(path_trim_end "$ar__directory")/" || return
	else
		ar__directory=""
	fi

	if test "$ar__prefix"; then
		ar__prefix="$(path_name "$ION_BIN_SELF")"-"$ar__prefix"
	fi

	if test "$ar__extension"; then
		ar__extension=".$ar__extension"
	fi

	printf \
		'%s%s-%s%s\n' \
		"$ar__directory" \
		"$SALT" \
		"$ar__prefix" \
		"$ar__extension"
}

start_temp_dir() {
	as__template="$(start_temp_template "$@")" || return
	dir_make "$as__template" || return
	print "$as__template"
}

start_temp_file() {
	at__template="$(start_temp_template "$@")" || return
	file_make "$at__template" || return
	print "$at__template"
}

start_temp_path() {
	da__template="$(start_temp_template "$@")" || return
	print "$da__template"
}

start_template() {
	:> "$TEMP_SED"
}

start_template_add() {
	print 's/'"$(string_escape "$1")"'/'"$(string_escape "$2")"'/g' >> "$TEMP_SED"
}

stop_template() {
	sed -f "$TEMP_SED"
}

start_lock() {
	# see: stackoverflow.com/a/24389468
	if have_flock; then
		exec 3>"$1" || return
		"$ION_BIN_FLOCK" -x 3
	fi
}

stop_lock() {
	if have_flock; then
		exec 3>&-
	fi
}

start_signal() {
	eg__ret=0
	start_lock "$TEMP_WATCH_LOCK" || return
	print "$1" >> "$TEMP_WATCH_STREAM" || eg__ret=$?
	stop_lock || return
	return $eg__ret
}

stop_signal() {
	ej__ret=0
	start_lock "$TEMP_WATCH_LOCK" || return
	sort -u < "$TEMP_WATCH_STREAM" | tr -d "$NEWLINE" || ej__ret=$?
	:> "$TEMP_WATCH_STREAM" || ej__ret=$?
	stop_lock || return
	return $ej__ret
}

start_fswatch() {
	ch__once="$1"; shift
	ch__args="-ro"
	ch__ret=0

	ch__ifs="$IFS"
	IFS=" "
	
	if test "$ch__once" = 1; then
		ch__args="$ch__args""1"
	fi

	if test "$ION_WATCH_THROTTLE"; then
		ch__args="$ch__args""l $ION_WATCH_THROTTLE"
	fi

	# shellcheck disable=SC2086
	start "$ION_BIN_FSWATCH" $ch__args "$@" >/dev/null || ch__ret=$?

	IFS="$ch__ifs"
	return $ch__ret
}

start_watch() {
	start_fswatch 1 "$@"
}

start_monitor() {
	if should_watch_initial; then
		start_signal "$1" || return
	fi

	cz__old_ifs="$IFS"
	IFS="$NEWLINE"

	# shellcheck disable=SC2046
	start_fswatch 0 $(cat) | {
		#if should_watch_initial; then
		#	IFS= read -r _
		#fi

		while IFS= read -r _; do
			start_signal "$1"
		done
	}

	IFS="$cz__old_ifs"
}

start_watcher() {
	note "$ION__MSG_STARTING_WATCHER"

	if test "$ION_INPUT" && ! test "$WATCHER_PID_INPUT"; then
		print "$ION_INPUT" | start_monitor "$ION___SIGNAL_INPUT" &
		WATCHER_PID_INPUT="$!"
	fi

	if test "$ION_SOURCE" && ! test "$WATCHER_PID_SOURCE"; then
		paths_split_raw "$ION_SOURCE" | start_monitor "$ION___SIGNAL_SOURCE" &
		WATCHER_PID_SOURCE="$!"
	fi
}

stop_watcher() {
	note "$ION__MSG_STOPPING_WATCHER"

	if test "$WATCHER_PID_INPUT"; then
		stop "$WATCHER_PID_INPUT" || true
		WATCHER_PID_INPUT=""
	fi

	if test "$WATCHER_PID_SOURCE"; then
		stop "$WATCHER_PID_SOURCE" || true
		WATCHER_PID_SOURCE=""
	fi
}

start_tcpserver() {
	cv__old_run_cmd="$ION_START_CMD"
	cv__old_run_args="$ION_START_ARGS"

	export ION_START_CMD="$ION_BIN_TCPSERVER"
	export ION_START_ARGS="-q -U -H -R 127.0.0.1 $ION_INBOX_PORT env"

	# shellcheck disable=SC2119
	start_bg || return

	SERVER_PID="$START_PID"

	export ION_START_CMD="$cv__old_run_cmd"
	export ION_START_ARGS="$cv__old_run_args"
}

stop_tcpserver() {
	if test "$SERVER_PID"; then
		stop "$SERVER_PID" || return
		SERVER_PID=""
	fi
}

start_caddy_protocol() {
	if test "$ION_SERVE_PRODUCTION"; then
		print https
	else
		print http
	fi
}

start_caddy_port() {
	if test "$ION_SERVE_PORT"; then
		print "$ION_SERVE_PORT"
	elif ! test "$ION_SERVE_PRODUCTION"; then
		print 8080
	fi
}

start_caddy_config_raw() {
	if ! test "$ION_SERVE_PORT" && test "$ION_SERVE_PRODUCTION"; then
		print "$TEMPLATE_CADDY_PRODUCTION"
	else
		print "$TEMPLATE_CADDY_DEVELOPMENT"
	fi

	print "$TEMPLATE_CADDY_SHARED"

	if test "$ION_SERVE_PRODUCTION"; then
		if test "$ION_SERVE_WWW"; then
			print "$TEMPLATE_CADDY_WWW"
		else
			print "$TEMPLATE_CADDY_UNWWW"
		fi
	fi
}

start_caddy_config() {
	start_template || return
	start_template_add ROOT_DIRECTORY "$ION_SERVED" || return
	start_template_add PROTOCOL "$(start_caddy_protocol)" || return
	start_template_add SERVE_PORT "$(start_caddy_port)" || return
	start_template_add DOMAIN_NAME "$ION_DOMAIN" || return
	start_template_add INBOX_PORT "$ION_INBOX_PORT" || return
	start_caddy_config_raw | stop_template || return
}

start_caddy() {
	cm__ret=0
	cm__temp="$(start_temp_file config caddyfile)" || return
	start_caddy_config > "$cm__temp" || return

	start "$ION_BIN_CADDY" reload --config "$cm__temp" 2>/dev/null || {
		if start "$ION_BIN_CADDY" start --config "$cm__temp" >/dev/null 2>&1; then
			SERVER_STARTED=1
		else
			cm__ret=$?
		fi
	}

	file_remove "$cm__temp"
	return $cm__ret
}

stop_caddy() {
	if test "$SERVER_STARTED"; then
		start "$ION_BIN_CADDY" stop 2>/dev/null || return
		SERVER_STARTED=
	else
		start "$ION_BIN_CADDY" reload --config - </dev/null >/dev/null 2>&1 || return
	fi
}

start_server() {
	note "$ION__MSG_STARTING_SERVERS"

	if ! have_server; then
		error "$ION__MSG_COMMAND_NOT_FOUND" "caddy and tcpserver" || return
		return 1
	fi

	if ! path_is_dir "$ION_SERVED"; then
		dir_make_all "$ION_SERVED" || return
	fi

	if have_tcpserver; then
		start_tcpserver || return
	fi

	if have_caddy; then
		start_caddy || return
	fi
}

stop_server() {
	note "$ION__MSG_STOPPING_SERVERS"

	if ! should_serve; then
		return 0
	fi

	if have_caddy; then
		stop_caddy || true
	fi

	if have_tcpserver; then
		stop_tcpserver || true
	fi
}

start_pandoc_internal() {
	ci__template="$1"; shift

	if test "$ci__template"; then
		start "$ION_BIN_PANDOC" --template="$ci__template" "$@"
	else
		start "$ION_BIN_PANDOC" "$@"
	fi
}

start_pandoc() {
	ax__filter="$1"; shift
	ax__path="$1"; shift
	ax__full="${1:-"$TEMP_BLANK"}"; shift
	ax__output="$1"; shift

	ax__ret=0
	ax__format="plain"
	ax__template=
	ax__target=
	ax__args=
	
	ax__ext="$(path_ext_get "$ax__full")" || return

	case "$ax__filter" in
		test)
			ax__filter="$TEMP_FILTER_TEST"
			ax__target="$ION__EXT_HTML"
		;;
		split)
			ax__filter="$TEMP_FILTER_SPLIT"
		;;
		merge)
			ax__filter="$TEMP_FILTER_MERGE"
			ax__template="$TEMP_TEMPLATE_JSON"
		;;
		partial)
			ax__filter="$TEMP_FILTER_DOCUMENT"
			ax__target="$ION__EXT_HTML"
			ax__format="html4"
		;;
		full)
			ax__args="--standalone"
			ax__filter="$TEMP_FILTER_TEMPLATE"
			ax__template="$TEMP_TEMPLATE_HTML"
			ax__target="$ION__EXT_HTML"
			ax__format="html4"
		;;
		extract)
			ax__filter="$TEMP_FILTER_EXTRACT"
			ax__target="$ION__EXT_JSON"
		;;
		sandbox)
			ax__args="--sandbox"
			ax__filter="$TEMP_FILTER_EMPTY"
			ax__target="md"
			ax__format="commonmark_x"
		;;
		*)
			ax__filter="$TEMP_FILTER_EMPTY"
		;;
	esac

	ax__args="$ax__args --quiet"
	ax__args="$ax__args --to=$ax__format"
	ax__args="$ax__args --number-sections"
	ax__args="$ax__args --shift-heading-level-by=1"
	ax__args="$ax__args --highlight-style=$ION_SYNTAX"
	ax__args="$ax__args --email-obfuscation=references"
	ax__args="$ax__args --reference-location=document"
	ax__args="$ax__args --preserve-tabs"
	ax__args="$ax__args --section-divs"
	ax__args="$ax__args --wrap=none"
	ax__args="$ax__args --mathml"

	if ext_markdown "$ax__ext"; then
		ax__args="$ax__args --from=markdown"
		ax__args="$ax__args+mark"
		ax__args="$ax__args+autolink_bare_uris"
		ax__args="$ax__args+short_subsuperscripts"
		ax__args="$ax__args+emoji"
		ax__args="$ax__args-raw_html"
	fi

	ax__old_path="$ION_FILTER_PATH"
	ax__old_output="$ION_FILTER_OUTPUT"
	ax__old_target="$ION_FILTER_TARGET"
	ax__old_ifs="$IFS"
	IFS=" "

	export ION_FILTER_PATH="$ax__path"
	export ION_FILTER_OUTPUT="$ax__output"
	export ION_FILTER_TARGET="$ax__target"

	# shellcheck disable=SC2086
	start_pandoc_internal \
		"$ax__template" \
		--lua-filter="$ax__filter" \
		"$ax__full" \
		$ax__args \
		"$@" || ax__ret=$?

	export ION_FILTER_PATH="$ax__old_path"
	export ION_FILTER_OUTPUT="$ax__old_output"
	export ION_FILTER_TARGET="$ax__old_target"

	IFS="$ax__old_ifs"
	return $ax__ret
}

start_pandoc_tests() {
	start_pandoc test "$TEMP_BLANK" "$TEMP_BLANK" "$TEMP_COMPILED" >/dev/null
}

start_pandoc_split() {
	start_pandoc split "" "" "$1"
}

start_pandoc_merge() {
	start_pandoc merge "" "" "$1"
}

start_pandoc_extract() {
	ae__path="$1"; shift
	ae__full="$1"; shift
	ae__output="$1"; shift
	start_pandoc extract "$ae__path" "$ae__full" "$ae__output" "$@"
}

start_pandoc_filter() {
	bw__path="$1"; shift
	bw__full="$1"; shift
	bw__output="$1"; shift
	start_pandoc partial "$bw__path" "$bw__full" "$bw__output" "$@"
}

start_pandoc_template() {
	by__path="$1"; shift
	by__full="$1"; shift
	by__output="$1"; shift
	start_pandoc full "$by__path" "$by__full" "$by__output" "$@"
}

start_pandoc_sandbox() {
	bt__path="$1"; shift
	bt__full="$1"; shift
	start_pandoc sandbox "$bt__path" "$bt__full" "" "$@"
}

start_tidy() {
	bh__body="$1"
	bh__path="$2"
	bh__result=0

	start "$ION_BIN_TIDY" \
		-quiet \
		-clean \
		--wrap 0 \
		--write-back yes \
		--add-xml-decl no \
		--output-xhtml yes \
		--show-body-only "$bh__body" \
		--add-meta-charset yes \
		--drop-empty-elements no \
		--drop-empty-paras no \
		--logical-emphasis yes \
		--indent-with-tabs yes \
		--indent auto \
		--merge-divs no \
		--merge-spans no \
		--join-styles yes \
		"$bh__path" || bh__result=$?

	if test "$bh__result" -gt 1; then
		return 1
	else
		return 0
	fi
}

start_tidy_partial() {
	start_tidy yes "$@"
}

start_tidy_standalone() {
	start_tidy no "$@"
}

start_esbuild() {
	if ! have_esbuild; then
		return 0
	fi

	de__in="$1"
	de__ret=0
	de__args=
	de__ifs="$IFS"
	IFS=" "

	if test "$ION_MINIFY" -gt 0; then
		de__args="$de__args --sourcemap=linked --sources-content=true --minify-whitespace --minify-syntax --minify-identifiers --keep-names"
	fi

	# shellcheck disable=SC2086
	start "$ION_BIN_ESBUILD" \
		--outdir="$TEMP_COMPILED" \
		--loader="$de__in" \
		--bundle \
		--target=es6 \
		--platform=browser \
		--loader:'.svg'=dataurl \
		--external:'*.png' \
		--external:'*.ttf' \
		--external:'*.otf' \
		--external:'*.woff' \
		--external:'*.woff2' \
		--log-level=error \
		--log-limit=0 \
		--color=false \
		$de__args || de__ret=$?

	IFS="$de__ifs"
	return $de__ret
}

start_esbuild_style() {
	start_esbuild css < "$TEMP_SOURCE_STYLES"
}

start_esbuild_script() {
	start_esbuild js < "$TEMP_SOURCE_SCRIPTS"
}

start_compile() {
	info "$ION__MSG_STARTING_COMPILER"
	dir_empty "$TEMP_COMPILED" || return
	start_esbuild_style || return
	start_esbuild_script || return
}

start_step() {
	eu__step="$1"
	eu__count="$2"

	start_find "$eu__step" 1 | while IFS= read -r eu__action_line; do
		eu__action_name="${eu__action_line#./}"
		eu__action_path="$eu__step/$eu__action_name"

		note "$ION__MSG_BUILD_STEP" "$eu__count" "$eu__action_name"

		start_find "$eu__action_path" | while IFS= read -r eu__path_line; do
			eu__path="/${eu__path_line#./}"
			eu__content_path="$eu__action_path""$eu__path"
			read -r eu__content < "$eu__content_path" || true
			printf '%s:%s:%s\n' "$eu__action_name" "$eu__path" "$eu__content"
		done
	done
}

start_run() {
	es__plan="$1"
	es__count=1

	while :; do
		es__step="$es__plan/$es__count"

		if test -d "$es__step"; then
			start_step "$es__step" "$es__count" | start_many "$ION_BIN_SELF" || exit
		else
			break
		fi

		es__count=$((es__count+1))
	done
}

start_scan() {
	start_find "$ION_INPUT" "$ION_CLUSTER" 1 | while IFS= read -r ep__line; do
		ep__ifs="$IFS"
		IFS=":"
		# shellcheck disable=SC2086
		set -- $ep__line
		IFS="$ep__ifs"

		ep__path_raw="${1:-}"
		ep__path="/${ep__path_raw#./}"
		ep__size="${2:-}"
		ep__time="$(floor "${3:-}")" || return
		ep__ext="$(path_ext_get "$ep__path")" || return
		ep__type="$(path_type "${4:-}" "$ep__ext")" || return
		ep__iteration=1

		printf \
			'%s:%s:%d:%d:%d\n' \
			"$ep__path" \
			"$ep__type" \
			"$ep__size" \
			"$ep__time" \
			"$ep__iteration"
	done
}

start_plan() {
	er__plan="$1"
	er__rebuild="$2"
	er__recompile="$3"
	er__ret=0

	er__step="$er__plan/1"
	er__index="$er__step/$ION__ACTION_INDEX"
	er__source="$er__step/$ION__ACTION_SOURCE"

	mkdir -- \
		"$er__step" \
		"$er__index" \
		"$er__source" \
	|| er__ret=$?

	if test "$er__ret" -ne 0; then
		return 1
	fi

	if test "$er__recompile"; then
		touch -- "$er__source/$ION__NAME_BRANCH"
	fi

	if test "$er__rebuild"; then
		start_scan | while IFS= read -r er__line; do
			er__ifs="$IFS"
			IFS=":"
	
			# shellcheck disable=SC2086
			set -- $er__line
	
			er__path="$1"; shift
			er__parent="$er__index""$(path_parent "$er__path")" || return
			er__indexed="$er__index""$er__path"
	
			mkdir -p -- "$er__parent" || continue
			printf '%s\n' "$*" > "$er__indexed" || continue
	
			IFS="$er__ifs"
		done
	fi

	return "$er__ret"
}

start_prune() {
	ey__time="$1"

	start_find "$ION_BUILD" 1 | while IFS= read -r ey__line; do
		ey__build="${ey__line#./}"
		ey__build_time="${ey__build%%-*}"

		if ! is_uint "$ey__build_time"; then
			continue
		fi

		if test "$ey__build_time" -lt "$ey__time"; then
			dir_remove "$ION_BUILD/$ey__build" || true
		fi
	done
}

start_build() {
	do__time="$1"
	do__rebuild="$2"
	do__recompile="$3"
	do__ret=0

	do__space="$(start_random 16)" || return
	do__build="$ION_BUILD/$do__time-$do__space"
	do__index="$do__build/$ION__NAME_ROOT"
	do__plan="$do__index/$ION__NAME_PLAN"

	mkdir -- \
		"$do__build" \
		"$do__index" \
		"$do__plan" \
	|| do__ret=$?

	if test "$do__ret" -eq 0; then
		export ION_BUILD_PREVIOUS=
		export ION_BUILD_CURRENT="$do__build"

		if test "$do__recompile"; then
			start_compile || return

			set +f
			mv -- "$TEMP_COMPILED"/* "$do__build" || do__ret=$?
			set -f

			test "$do__ret" -ne 0 && return "$do__ret"
		fi

		start_plan "$do__plan" "$do__rebuild" "$do__recompile" || return
		start_run "$do__plan" || return
	fi

	dir_remove "$do__plan" || return
	start_prune "$do__time" || return

	return "$do__ret"
}

start_build_logged() {
	dm__input="$1"
	dm__source="$2"
	dm__ret=0
	dm__pid=

	dm__before="$(timestamp)" || return

	if test -w "$ION_BUILD_LOG"; then
		:> "$ION_BUILD_LOG" || return
		start_build "$dm__before" "$dm__input" "$dm__source" > "$ION_BUILD_LOG" 2>&1 &
		dm__pid=$!
	else
		start_build "$dm__before" "$dm__input" "$dm__source" &
		dm__pid=$!
	fi

	wait "$dm__pid" || dm__ret=$?
	
	dm__after="$(timestamp)" || return
	dm__duration=$((dm__after-dm__before)) || return
	note "$ION__MSG_STOPPING_BUILD" "$dm__duration""$ION___SUFFIX_SECONDS"

	if test "$dm__ret" -eq 0 && test -w "$ION_BUILD_LOG" && ! test "$ION_BUILD_LOG_KEEP"; then
		file_remove "$ION_BUILD_LOG" || return
	fi
}

start_build_bounce() {
	ef__result=0

	sleep "$ION_WATCH_DEBOUNCE" 2>/dev/null || ef__result=$?

	if test "$ef__result" -ne 0 && is_unum "$ION_WATCH_DEBOUNCE"; then
		note "$ION__MSG_NOT_SLEEPING"
		ION_WATCH_DEBOUNCE="$(floor "$ION_WATCH_DEBOUNCE")" || return
		sleep "$ION_WATCH_DEBOUNCE" || return
	fi
}

start_build_debounce() {
	if ! test "$ION_WATCH_DEBOUNCE"; then
		return
	fi

	dp__size=0

	note "$ION__MSG_CHANGED"

	while :; do
		start_build_bounce || return

		dp__size_new="$(start_size "$TEMP_WATCH_STREAM")" || return

		if test "$dp__size_new" = "$dp__size"; then
			break
		else
			dp__size="$dp__size_new"
		fi
	done
}

start_build_receiving() {
	dn__first="$1"
	dn__size=0
	dn__pid=
	
	if should_watch; then
		start_watch "$TEMP_WATCH_STREAM" &
		dn__pid=$!

		dn__size="$(start_size "$TEMP_WATCH_STREAM")" || return
	fi

	if test "$dn__pid"; then
		if test "$dn__size" -eq 0; then
			wait "$dn__pid" || return
			dn__size="$(start_size "$TEMP_WATCH_STREAM")" || return
		else
			stop "$dn__pid" || return
		fi
	fi

	dn__change="$(stop_signal)" || return

	if ! test "$dn__change"; then
		return
	fi

	case "$dn__change" in
		*"$ION___SIGNAL_INPUT"*) dn__input=1 ;;
		*) dn__input= ;;
	esac

	case "$dn__change" in
		*"$ION___SIGNAL_SOURCE"*) dn__source=1 ;;
		*) dn__source= ;;
	esac

	if ! test "$dn__first"; then
		start_build_debounce || return
	fi

	start_build_logged "$dn__input" "$dn__source"
}

start_builder() {
	dj__first=1
	
	if should_watch; then
		while :; do
			if ! test "$dj__first"; then
				terminal_clear || true
			fi

			start_build_receiving "$dj__first" || return

			dj__first=
		done
	else
		start_build_logged "" "" || return
	fi
}

start_building() {
	# for each $@
	note "$ION__MSG_BUILD_ACTION" "$@"
}

stop_temp() {
	file_remove "$TEMP_SED" || true
	file_remove "$TEMP_BLANK" || true
	file_remove "$TEMP_WATCH_LOCK" || true
	file_remove "$TEMP_WATCH_STREAM" || true
	file_remove "$TEMP_FILTER_EMPTY" || true
	file_remove "$TEMP_FILTER_TEST" || true
	file_remove "$TEMP_FILTER_SPLIT" || true
	file_remove "$TEMP_FILTER_MERGE" || true
	file_remove "$TEMP_FILTER_EXTRACT" || true
	file_remove "$TEMP_FILTER_DOCUMENT" || true
	file_remove "$TEMP_FILTER_TEMPLATE" || true
	file_remove "$TEMP_TEMPLATE_JSON" || true
	file_remove "$TEMP_TEMPLATE_HTML" || true
	file_remove "$TEMP_SOURCE_STYLES" || true
	file_remove "$TEMP_SOURCE_SCRIPTS" || true

	if path_is_dir "$TEMP_COMPILED"; then
		dir_remove "$TEMP_COMPILED" || true
	fi
	
	if test "$BUILD_TEMP"; then
		dir_remove "$ION_BUILD" || true
		BUILD_TEMP=
	fi
	
	if test "$SERVED_TEMP"; then
		dir_remove "$ION_SERVED" || true
		SERVED_TEMP=
	fi
}

deinit_main() {
	stop_watcher || true
	stop_server || true
	stop_temp || true
}

deinit() {
	if ! have_parent && test "$STARTED"; then
		deinit_main
		STARTED=
	fi
}

init_signals() {
	trap deinit EXIT HUP INT QUIT ABRT TERM
	trap "" PIPE
}

init_basics() {
	TAB="$(printf '\t')" || return
	CARRIAGE="$(printf '\r_')" || return
	CARRIAGE="${CARRIAGE%_}"
	NEWLINE="
"
}

init_env_bin() {
	cs__command="$1"
	cs__self="$2"

	cs__absolute=

	if path_is_absolute "$cs__command"; then
		cs__absolute="$cs__command"
	elif path_is_name "$cs__command" && ! test -f "$cs__command"; then
		cs__absolute="$(find_command "$cs__command")" || return
	elif test "$cs__command"; then
		cs__absolute="$(path_normal "$cs__command")" || return
	fi
	
	if path_is_name "$cs__absolute"; then
		cs__self=1
	fi

	if path_is_absolute "$cs__absolute" && ! test "$cs__self" && ! path_is_exec "$cs__absolute"; then
		error "$ION__MSG_COMMAND_NOT_EXEC" "$cs__command" "$cs__absolute" || return
	fi
	
	if ! test "$cs__absolute" || path_is_safe "$cs__absolute"; then
		print "$cs__absolute"
	else
		error "$ION__MSG_COMMAND_NOT_RECOGNISED" "$cs__absolute" || return
	fi
}

init_env_bins() {
	dc__commands="$1"; shift

	paths_split_raw "$dc__commands" | {
		dc__found=

		while IFS= read -r dc__command; do
			dc__found="$(init_env_bin "$dc__command" "$@")" || continue

			if test "$dc__found"; then
				break
			fi
		done

		print "$dc__found"
	}
}

init_env_find() {
	ea__bin_self="$(init_env_bins "$ION_BIN_SELF" 1)" || return
	ea__bin_flock="$(init_env_bins "$ION_BIN_FLOCK")" || return
	ea__bin_caddy="$(init_env_bins "$ION_BIN_CADDY")" || return
	ea__bin_find="$(init_env_bins "$ION_BIN_FIND")" || return
	ea__bin_fswatch="$(init_env_bins "$ION_BIN_FSWATCH")" || return
	ea__bin_esbuild="$(init_env_bins "$ION_BIN_ESBUILD")" || return
	ea__bin_ln="$(init_env_bins "$ION_BIN_LN")" || return
	ea__bin_luac="$(init_env_bins "$ION_BIN_LUAC")" || return
	ea__bin_openssl="$(init_env_bins "$ION_BIN_OPENSSL")" || return
	ea__bin_pandoc="$(init_env_bins "$ION_BIN_PANDOC")" || return
	ea__bin_rclone="$(init_env_bins "$ION_BIN_RCLONE")" || return
	ea__bin_sha256sum="$(init_env_bins "$ION_BIN_SHA256SUM")" || return
	ea__bin_sha256="$(init_env_bins "$ION_BIN_SHA256")" || return
	ea__bin_shasum="$(init_env_bins "$ION_BIN_SHASUM")" || return
	ea__bin_shellcheck="$(init_env_bins "$ION_BIN_SHELLCHECK")" || return
	ea__bin_ssh="$(init_env_bins "$ION_BIN_SSH")" || return
	ea__bin_stat="$(init_env_bins "$ION_BIN_STAT")" || return
	ea__bin_tcpserver="$(init_env_bins "$ION_BIN_TCPSERVER")" || return
	ea__bin_tidy="$(init_env_bins "$ION_BIN_TIDY")" || return
	ea__bin_xargs="$(init_env_bins "$ION_BIN_XARGS")" || return
	ea__bin_parallel="$(init_env_bins "$ION_BIN_PARALLEL")" || return

	export ION_BIN_SELF="$ea__bin_self"
	export ION_BIN_FLOCK="$ea__bin_flock"
	export ION_BIN_CADDY="$ea__bin_caddy"
	export ION_BIN_FIND="$ea__bin_find"
	export ION_BIN_FSWATCH="$ea__bin_fswatch"
	export ION_BIN_ESBUILD="$ea__bin_esbuild"
	export ION_BIN_LN="$ea__bin_ln"
	export ION_BIN_LUAC="$ea__bin_luac"
	export ION_BIN_OPENSSL="$ea__bin_openssl"
	export ION_BIN_PANDOC="$ea__bin_pandoc"
	export ION_BIN_RCLONE="$ea__bin_rclone"
	export ION_BIN_SHA256SUM="$ea__bin_sha256sum"
	export ION_BIN_SHA256="$ea__bin_sha256"
	export ION_BIN_SHASUM="$ea__bin_shasum"
	export ION_BIN_SHELLCHECK="$ea__bin_shellcheck"
	export ION_BIN_SSH="$ea__bin_ssh"
	export ION_BIN_STAT="$ea__bin_stat"
	export ION_BIN_TCPSERVER="$ea__bin_tcpserver"
	export ION_BIN_TIDY="$ea__bin_tidy"
	export ION_BIN_XARGS="$ea__bin_xargs"
	export ION_BIN_PARALLEL="$ea__bin_parallel"
}

init_env_input() {
	if test "$ION_INPUT"; then
		dt__normal="$(path_normal "$ION_INPUT")" || return
		export ION_INPUT="$dt__normal"
	fi
}

init_env_styles() {
	paths_split_raw "$1" | {
		eb__styles=
		
		while IFS= read -r eb__path; do
			if path_is_dir "$eb__path"; then
				eb__path="$(path_join "$eb__path" "$ION__NAME_MAIN_CSS")" || continue
			fi

			eb__ext="$(path_ext_get "$eb__path")" || continue

			if path_is_file "$eb__path" && ext_style "$eb__ext"; then
				eb__styles="$(paths_join "$eb__styles" "$eb__path")"
			fi
		done
		
		print "$eb__styles"
	}
}

init_env_scripts() {
	paths_split_raw "$1" | {
		ec__scripts=

		while IFS= read -r ec__path; do
			if path_is_dir "$ec__path"; then
				ec__path="$(path_join "$ec__path" "$ION__NAME_MAIN_JS")" || continue
			fi

			ec__ext="$(path_ext_get "$ec__path")" || continue

			if path_is_file "$ec__path" && ext_script "$ec__ext"; then
				ec__scripts="$(paths_join "$ec__scripts" "$ec__path")"
			fi
		done

		print "$ec__scripts"
	}
}

init_env_source() {
	if test "$ION_SOURCE"; then
		dg__source="$(paths_normal "$ION_SOURCE")" || return
		export ION_SOURCE="$dg__source"

		if ! test "$ION_SOURCE_STYLES"; then
			dg__styles="$(init_env_styles "$dg__source")" || return
			export ION_SOURCE_STYLES="$dg__styles"
		fi

		if ! test "$ION_SOURCE_SCRIPTS"; then
			dg__scripts="$(init_env_scripts "$dg__source")" || return
			export ION_SOURCE_SCRIPTS="$dg__scripts"
		fi
	fi
}

init_env_build() {
	if test "$ION_BUILD"; then
		du__normal="$(path_normal "$ION_BUILD")" || return
		export ION_BUILD="$du__normal"
	fi
}

init_env_served() {
	if test "$ION_SERVED"; then
		dx__normal="$(path_normal "$ION_SERVED")" || return
		export ION_SERVED="$dx__normal"
	fi
}

init_env_mirrors() {
	if test "$ION_MIRRORS"; then
		dy__normal="$(paths_normal "$ION_MIRRORS")" || return
		export ION_MIRRORS="$dy__normal"
	fi
}

init_env_inbox() {
	if test "$ION_INBOX"; then
		dz__normal="$(paths_normal "$ION_INBOX")" || return
		export ION_INBOX="$dz__normal"
	fi
}

init_env() {
	init_env_find || return
	init_env_source || return
	init_env_input || return
	init_env_build || return
	init_env_served || return
	init_env_mirrors || return
	init_env_inbox || return
}

init_check_posix() {
	if ! found_posix; then
		error "$ION__MSG_NOT_POSIX" || return
	fi
}

init_check_bool() {
	if ! is_bool "$2"; then
		error "$ION__MSG_INVALID_ENVIRONMENT" "$1" "$2" || return
	fi
}

init_check_uint() {
	if ! is_uint "$2"; then
		error "$ION__MSG_INVALID_ENVIRONMENT" "$1" "$2" || return
	fi
}

init_check_int() {
	if ! is_int "$2"; then
		error "$ION__MSG_INVALID_ENVIRONMENT" "$1" "$2" || return
	fi
}

init_check_unum() {
	if ! is_unum "$2"; then
		error "$ION__MSG_INVALID_ENVIRONMENT" "$1" "$2" || return
	fi
}

init_check_num() {
	if ! is_num "$2"; then
		error "$ION__MSG_INVALID_ENVIRONMENT" "$1" "$2" || return
	fi
}

init_check_string() {
	if ! test "$2" || ! string_is_safe "$2"; then
		error "$ION__MSG_INVALID_ENVIRONMENT" "$1" "$2" || return
	fi
}

init_check_name() {
	if ! test "$2" || ! path_is_safe "$2" || ! name_is_safe "$2"; then
		error "$ION__MSG_INVALID_ENVIRONMENT" "$1" "$2" || return
	fi
}

init_check_dir() {
	if ! path_is_dir "$2" || ! path_is_safe "$2"; then
		error "$ION__MSG_INVALID_ENVIRONMENT" "$1" "$2" || return
	fi
}

init_check_path() {
	if ! test "$2" || ! path_is_safe "$2"; then
		error "$ION__MSG_INVALID_ENVIRONMENT" "$1" "$2" || return
	fi
}

init_check_paths() {
	true
}

init_check_array() {
	true
}

init_check_array_name() {
	true
}

init_check_array_path() {
	true
}

init_check_array_paths() {
	true
}

init_check_command() {
	if ! "$1"; then
		error "$ION__MSG_COMMAND_NOT_FOUND" "$2" || return
	fi
}

init_check_env() {
	init_check_dir ION_TEMP "$ION_TEMP" || return

	init_check_string ION___ERROR_PREFIX_MAIN "$ION___ERROR_PREFIX_MAIN" || return
	init_check_string ION___ERROR_PREFIX_SUB "$ION___ERROR_PREFIX_SUB" || return
	init_check_string ION___ERROR_PREFIX_SUBL "$ION___ERROR_PREFIX_SUBL" || return
	init_check_string ION___ERROR_PREFIX_SUBR "$ION___ERROR_PREFIX_SUBR" || return
	init_check_string ION___ERROR_INFIX_MAIN "$ION___ERROR_INFIX_MAIN" || return
	init_check_string ION___ERROR_INFIX_SUB "$ION___ERROR_INFIX_SUB" || return

	init_check_string ION___SUFFIX_SECONDS "$ION___SUFFIX_SECONDS" || return

	init_check_string ION___QUERY_PHRASE "$ION___QUERY_PHRASE" || return
	init_check_string ION___QUERY_CLAUSE "$ION___QUERY_CLAUSE" || return
	init_check_string ION___QUERY_SENTENCE "$ION___QUERY_SENTENCE" || return
	init_check_string ION___QUERY_PARAGRAPH "$ION___QUERY_PARAGRAPH" || return
	init_check_uint ION___QUERY_SUBJECT "$ION___QUERY_SUBJECT" || return
	init_check_uint ION___QUERY_VERB "$ION___QUERY_VERB" || return
	init_check_string ION___QUERY_ALL "$ION___QUERY_ALL" || return
	init_check_string ION___QUERY_SEPARATOR "$ION___QUERY_SEPARATOR" || return
	init_check_string ION___QUERY_NESTED_LEFT "$ION___QUERY_NESTED_LEFT" || return
	init_check_string ION___QUERY_NESTED_RIGHT "$ION___QUERY_NESTED_RIGHT" || return

	init_check_string ION___TITLE_SEPARATOR "$ION___TITLE_SEPARATOR" || return
	init_check_string ION___TYPE_SEPARATOR "$ION___TYPE_SEPARATOR" || return

	init_check_string ION___SIGNAL_INPUT "$ION___SIGNAL_INPUT" || return
	init_check_string ION___SIGNAL_SOURCE "$ION___SIGNAL_SOURCE" || return

	init_check_name ION__EXT_JS "$ION__EXT_JS" || return
	init_check_name ION__EXT_CSS "$ION__EXT_CSS" || return
	init_check_name ION__EXT_HTML "$ION__EXT_HTML" || return
	init_check_name ION__EXT_JSON "$ION__EXT_JSON" || return

	init_check_name ION__WORD_INFO "$ION__WORD_INFO" || return
	init_check_name ION__WORD_NOTE "$ION__WORD_NOTE" || return
	init_check_name ION__WORD_ERROR "$ION__WORD_ERROR" || return
	init_check_name ION__WORD_INBOX "$ION__WORD_INBOX" || return
	init_check_name ION__WORD_INDEX "$ION__WORD_INDEX" || return
	init_check_name ION__WORD_BUILD "$ION__WORD_BUILD" || return
	init_check_name ION__WORD_SOURCE "$ION__WORD_SOURCE" || return
	init_check_name ION__WORD_MAIN "$ION__WORD_MAIN" || return

	init_check_name ION__NAME_PLAN "$ION__NAME_PLAN" || return
	init_check_name ION__NAME_ROOT "$ION__NAME_ROOT" || return
	init_check_name ION__NAME_BRANCH "$ION__NAME_BRANCH" || return
	init_check_name ION__NAME_INDEX_CSS "$ION__NAME_INDEX_CSS" || return
	init_check_name ION__NAME_INDEX_JS "$ION__NAME_INDEX_JS" || return
	init_check_name ION__NAME_MAIN_CSS "$ION__NAME_MAIN_CSS" || return
	init_check_name ION__NAME_MAIN_JS "$ION__NAME_MAIN_JS" || return

	init_check_string ION__MSG_CHANGING_DIR "$ION__MSG_CHANGING_DIR" || return
	init_check_string ION__MSG_COMMAND_NOT_FOUND "$ION__MSG_COMMAND_NOT_FOUND" || return
	init_check_string ION__MSG_COMMAND_NOT_EXEC "$ION__MSG_COMMAND_NOT_EXEC" || return
	init_check_string ION__MSG_COMMAND_NOT_RECOGNISED "$ION__MSG_COMMAND_NOT_RECOGNISED" || return
	init_check_string ION__MSG_INVALID_ENVIRONMENT "$ION__MSG_INVALID_ENVIRONMENT" || return
	init_check_string ION__MSG_INVALID_REPLACEMENT "$ION__MSG_INVALID_REPLACEMENT" || return
	init_check_string ION__MSG_LINKING_FILE "$ION__MSG_LINKING_FILE" || return
	init_check_string ION__MSG_NOT_SLEEPING "$ION__MSG_NOT_SLEEPING" || return
	init_check_string ION__MSG_CHANGED "$ION__MSG_CHANGED" || return
	init_check_string ION__MSG_MAKING_DIR "$ION__MSG_MAKING_DIR" || return
	init_check_string ION__MSG_MAKING_FILE "$ION__MSG_MAKING_FILE" || return
	init_check_string ION__MSG_MOVING_FILE "$ION__MSG_MOVING_FILE" || return
	init_check_string ION__MSG_NOT_POSIX "$ION__MSG_NOT_POSIX" || return
	init_check_string ION__MSG_REMOVING_FILE "$ION__MSG_REMOVING_FILE" || return
	init_check_string ION__MSG_RUNNING_COMMAND "$ION__MSG_RUNNING_COMMAND" || return
	init_check_string ION__MSG_RUNNING_COMMAND_BG "$ION__MSG_RUNNING_COMMAND_BG" || return
	init_check_string ION__MSG_RUNNING_COMMAND_MANY "$ION__MSG_RUNNING_COMMAND_MANY" || return
	init_check_string ION__MSG_RUNNING_TESTS "$ION__MSG_RUNNING_TESTS" || return
	init_check_string ION__MSG_NOTICED_CHANGE "$ION__MSG_NOTICED_CHANGE" || return
	init_check_string ION__MSG_BUILD_STEP "$ION__MSG_BUILD_STEP" || return
	init_check_string ION__MSG_BUILD_ACTION "$ION__MSG_BUILD_ACTION" || return
	init_check_string ION__MSG_STARTING_COMPILER "$ION__MSG_STARTING_COMPILER" || return
	init_check_string ION__MSG_STARTING_SERVERS "$ION__MSG_STARTING_SERVERS" || return
	init_check_string ION__MSG_STARTING_WATCHER "$ION__MSG_STARTING_WATCHER" || return
	init_check_string ION__MSG_STOPPING_WATCHER "$ION__MSG_STOPPING_WATCHER" || return
	init_check_string ION__MSG_STOPPING_SERVERS "$ION__MSG_STOPPING_SERVERS" || return
	init_check_string ION__MSG_STOPPING_BUILD "$ION__MSG_STOPPING_BUILD" || return
	init_check_string ION__MSG_OPENING_FILE "$ION__MSG_OPENING_FILE" || return
	init_check_string ION__MSG_QUERYING_THE_INDEX "$ION__MSG_QUERYING_THE_INDEX" || return
	init_check_string ION__MSG_QUERY_FOUND_AN_ENTRY "$ION__MSG_QUERY_FOUND_AN_ENTRY" || return

	init_check_name ION__ACTION_SOURCE "$ION__ACTION_SOURCE" || return
	init_check_name ION__ACTION_INDEX "$ION__ACTION_INDEX" || return

	init_check_name ION__VERB_IDENTITY "$ION__VERB_IDENTITY" || return

	init_check_name ION__TYPE_TRUE "$ION__TYPE_TRUE" || return
	init_check_name ION__TYPE_FALSE "$ION__TYPE_FALSE" || return
	init_check_name ION__TYPE_ARRAY "$ION__TYPE_ARRAY" || return
	init_check_name ION__TYPE_OBJECT "$ION__TYPE_OBJECT" || return
	init_check_name ION__TYPE_STRING "$ION__TYPE_STRING" || return
	init_check_name ION__TYPE_NUMBER "$ION__TYPE_NUMBER" || return
	init_check_name ION__TYPE_BOOLEAN "$ION__TYPE_BOOLEAN" || return
	init_check_name ION__TYPE_NULL "$ION__TYPE_NULL" || return
	init_check_name ION__TYPE_TEXT "$ION__TYPE_TEXT" || return
	init_check_name ION__TYPE_PATH "$ION__TYPE_PATH" || return
	init_check_name ION__TYPE_PATHS "$ION__TYPE_PATHS" || return
	init_check_name ION__TYPE_REFERENCE "$ION__TYPE_REFERENCE" || return
	init_check_name ION__TYPE_RELATIONSHIP "$ION__TYPE_RELATIONSHIP" || return

	init_check_name ION__META_TYPE "$ION__META_TYPE" || return
	init_check_name ION__META_TYPE_FILE "$ION__META_TYPE_FILE" || return
	init_check_name ION__META_TYPE_DIRECTORY "$ION__META_TYPE_DIRECTORY" || return
	init_check_name ION__META_TYPE_DATA "$ION__META_TYPE_DATA" || return
	init_check_name ION__META_TYPE_DOCUMENT "$ION__META_TYPE_DOCUMENT" || return
	init_check_name ION__META_TYPE_CODE "$ION__META_TYPE_CODE" || return
	init_check_name ION__META_TYPE_STYLE "$ION__META_TYPE_STYLE" || return
	init_check_name ION__META_TYPE_SCRIPT "$ION__META_TYPE_SCRIPT" || return
	init_check_name ION__META_TYPE_IMAGE "$ION__META_TYPE_IMAGE" || return
	init_check_name ION__META_TYPE_AUDIO "$ION__META_TYPE_AUDIO" || return
	init_check_name ION__META_TYPE_VIDEO "$ION__META_TYPE_VIDEO" || return
	init_check_name ION__META_TYPE_OBJECT "$ION__META_TYPE_OBJECT" || return
	init_check_name ION__META_TYPE_MAP "$ION__META_TYPE_MAP" || return
	init_check_name ION__META_TYPE_FONT "$ION__META_TYPE_FONT" || return
	init_check_name ION__META_SCAN "$ION__META_SCAN" || return
	init_check_name ION__META_PATH "$ION__META_PATH" || return
	init_check_name ION__META_NAME "$ION__META_NAME" || return
	init_check_name ION__META_EXTENSION "$ION__META_EXTENSION" || return
	init_check_name ION__META_DOMAIN "$ION__META_DOMAIN" || return
	init_check_name ION__META_HASH "$ION__META_HASH" || return
	init_check_name ION__META_STAMP "$ION__META_STAMP" || return
	init_check_name ION__META_SIGNER "$ION__META_SIGNER" || return
	init_check_name ION__META_SIGNATURE "$ION__META_SIGNATURE" || return
	init_check_name ION__META_SALT "$ION__META_SALT" || return
	init_check_name ION__META_LOCATION "$ION__META_LOCATION" || return
	init_check_name ION__META_ITERATION "$ION__META_ITERATION" || return
	init_check_name ION__META_MODIFIED "$ION__META_MODIFIED" || return
	init_check_name ION__META_SIZE "$ION__META_SIZE" || return
	init_check_name ION__META_WIDTH "$ION__META_WIDTH" || return
	init_check_name ION__META_HEIGHT "$ION__META_HEIGHT" || return
	init_check_name ION__META_DEPTH "$ION__META_DEPTH" || return
	init_check_name ION__META_LENGTH "$ION__META_LENGTH" || return
	init_check_name ION__META_DIRECTION "$ION__META_DIRECTION" || return
	init_check_name ION__META_ENCODING "$ION__META_ENCODING" || return
	init_check_name ION__META_ALPHABET "$ION__META_ALPHABET" || return
	init_check_name ION__META_LANGUAGE "$ION__META_LANGUAGE" || return
	init_check_name ION__META_WORDMARK "$ION__META_WORDMARK" || return
	init_check_name ION__META_FLAG "$ION__META_FLAG" || return
	init_check_name ION__META_ICON "$ION__META_ICON" || return
	init_check_name ION__META_TITLE "$ION__META_TITLE" || return
	init_check_name ION__META_DESCRIPTION "$ION__META_DESCRIPTION" || return
	init_check_name ION__META_PUBLISHED "$ION__META_PUBLISHED" || return
	init_check_name ION__META_AUTHORS "$ION__META_AUTHORS" || return
	init_check_name ION__META_ALBUM "$ION__META_ALBUM" || return
	init_check_name ION__META_PUBLIC "$ION__META_PUBLIC" || return
	init_check_name ION__META_COVER "$ION__META_COVER" || return
	init_check_name ION__META_CATEGORY "$ION__META_CATEGORY" || return
	init_check_name ION__META_LABELS "$ION__META_LABELS" || return
	init_check_name ION__META_FROM "$ION__META_FROM" || return
	init_check_name ION__META_PARENT "$ION__META_PARENT" || return
	init_check_name ION__META_RELATED "$ION__META_RELATED" || return
	init_check_name ION__META_PREVIOUSLY "$ION__META_PREVIOUSLY" || return
	init_check_name ION__META_REFERENCES "$ION__META_REFERENCES" || return
	init_check_name ION__META_DEPENDENCIES "$ION__META_DEPENDENCIES" || return
	init_check_name ION__META_TRANSLATIONS "$ION__META_TRANSLATIONS" || return
	init_check_name ION__META_DERIVATIVES "$ION__META_DERIVATIVES" || return

	init_check_string ION__ATTR_CURRENT "$ION__ATTR_CURRENT" || return
	init_check_string ION__ATTR_QUERY "$ION__ATTR_QUERY" || return
	init_check_string ION__ATTR_MULTIPLE "$ION__ATTR_MULTIPLE" || return
	init_check_string ION__ATTR_NESTED "$ION__ATTR_NESTED" || return
	init_check_string ION__ATTR_KEY "$ION__ATTR_KEY" || return
	init_check_string ION__ATTR_TYPE "$ION__ATTR_TYPE" || return
	init_check_string ION__ATTR_VALUE "$ION__ATTR_VALUE" || return
	init_check_string ION__ATTR_NAME "$ION__ATTR_NAME" || return

	init_check_name ION__CLASS_INDEX "$ION__CLASS_INDEX" || return
	init_check_name ION__CLASS_WORD "$ION__CLASS_WORD" || return
	init_check_name ION__CLASS_PAGE "$ION__CLASS_PAGE" || return
	init_check_name ION__CLASS_FORM "$ION__CLASS_FORM" || return
	init_check_name ION__CLASS_HEADER "$ION__CLASS_HEADER" || return
	init_check_name ION__CLASS_REQUIRED "$ION__CLASS_REQUIRED" || return
	init_check_name ION__CLASS_COMPONENT "$ION__CLASS_COMPONENT" || return
	init_check_name ION__CLASS_NO_JS "$ION__CLASS_NO_JS" || return

	init_check_path ION_DEV_URANDOM "$ION_DEV_URANDOM" || return

	init_check_uint ION_SERVE "$ION_SERVE" || return
	! test "$ION_SERVED" || init_check_dir ION_SERVED "$ION_SERVED" || return
	init_check_bool ION_SERVE_WWW "$ION_SERVE_WWW" || return
	init_check_bool ION_SERVE_PRODUCTION "$ION_SERVE_PRODUCTION" || return
	! test "$ION_SERVE_PORT" || init_check_uint ION_SERVE_PORT "$ION_SERVE_PORT" || return

	init_check_uint ION_WATCH "$ION_WATCH" || return
	init_check_bool ION_WATCH_INITIAL "$ION_WATCH_INITIAL" || return
	! test "$ION_WATCH_THROTTLE" || init_check_unum ION_WATCH_THROTTLE "$ION_WATCH_THROTTLE" || return
	! test "$ION_WATCH_DEBOUNCE" || init_check_unum ION_WATCH_DEBOUNCE "$ION_WATCH_DEBOUNCE" || return
	init_check_bool ION_WATCH_CLEAR "$ION_WATCH_CLEAR" || return

	! test "$ION_BUILD" || init_check_dir ION_BUILD "$ION_BUILD" || return
	! test "$ION_BUILD_CURRENT" || init_check_dir ION_BUILD_CURRENT "$ION_BUILD_CURRENT" || return
	! test "$ION_BUILD_PREVIOUS" || init_check_dir ION_BUILD_PREVIOUS "$ION_BUILD_PREVIOUS" || return
	! test "$ION_BUILD_LOG" || init_check_path ION_BUILD_LOG "$ION_BUILD_LOG" || return
	init_check_bool ION_BUILD_LOG_KEEP "$ION_BUILD_LOG_KEEP" || return
	init_check_bool ION_BUILD_JS "$ION_BUILD_JS" || return
	init_check_bool ION_BUILD_JS_GLOBAL "$ION_BUILD_JS_GLOBAL" || return
	init_check_bool ION_BUILD_CSS "$ION_BUILD_CSS" || return
	init_check_bool ION_BUILD_CSS_GLOBAL "$ION_BUILD_CSS_GLOBAL" || return
	init_check_bool ION_BUILD_HTML "$ION_BUILD_HTML" || return

	! test "$ION_INBOX" || init_check_dir ION_INBOX "$ION_INBOX" || return
	init_check_uint ION_INBOX_PORT "$ION_INBOX_PORT" || return
	init_check_uint ION_INBOX_RATES "$ION_INBOX_RATES" || return

	init_check_path ION_LINK_PREFIX "$ION_LINK_PREFIX" || return
	init_check_bool ION_LINK_PROTOCOL "$ION_LINK_PROTOCOL" || return
	init_check_bool ION_LINK_DOMAIN "$ION_LINK_DOMAIN" || return
	init_check_bool ION_LINK_TRIM "$ION_LINK_TRIM" || return

	init_check_uint ION_EXTRACT_MAXIMUM "$ION_EXTRACT_MAXIMUM" || return
	! test "$ION_EXTRACT_SUFFIX" || init_check_string ION_EXTRACT_SUFFIX "$ION_EXTRACT_SUFFIX" || return

	init_check_paths ION_SOURCE "$ION_SOURCE" || return
	init_check_paths ION_SOURCE_STYLES "$ION_SOURCE_STYLES" || return
	init_check_paths ION_SOURCE_SCRIPTS "$ION_SOURCE_SCRIPTS" || return

	init_check_uint ION_START_ID "$ION_START_ID" || return
	init_check_string ION_START_CMD "$ION_START_CMD" || return
	! test "$ION_START_ARGS" || init_check_string ION_START_ARGS "$ION_START_ARGS" || return

	init_check_name ION_DOMAIN "$ION_DOMAIN" || return
	init_check_string ION_SYNTAX "$ION_SYNTAX" || return
	init_check_array_paths ION_COGNATES "$ION_COGNATES" || return

	init_check_dir ION_TEMP "$ION_TEMP" || return
	init_check_bool ION_TEST "$ION_TEST" || return
	init_check_bool ION_WORDS "$ION_WORDS" || return

	init_check_uint ION_VOLUME "$ION_VOLUME" || return
	init_check_uint ION_MINIFY "$ION_MINIFY" || return
	! test "$ION_PARALLEL" || init_check_uint ION_PARALLEL "$ION_PARALLEL" || return
	init_check_bool ION_CLUSTER "$ION_CLUSTER" || return

	! test "$ION_INPUT" || init_check_dir ION_INPUT "$ION_INPUT" || return
	init_check_paths ION_MIRRORS "$ION_MIRRORS" || return
}

init_check_bsd() {
	if test "$ION_BIN_STAT" && ! test "$ION_BIN_STAT_BSD"; then
		if test "$("$ION_BIN_STAT" -f '%z' /dev/null 2>/dev/null)" = 0; then
			export ION_BIN_STAT_BSD=1
		else
			export ION_BIN_STAT_BSD=0
		fi
	fi
}

init_check_gnu() {
	if test "$ION_BIN_STAT" && ! test "$ION_BIN_STAT_GNU"; then
		if test "$("$ION_BIN_STAT" -c '%s' /dev/null 2>/dev/null)" = 0; then
			export ION_BIN_STAT_GNU=1
		else
			export ION_BIN_STAT_GNU=0
		fi
	fi

	if test "$ION_BIN_FIND" && ! test "$ION_BIN_FIND_GNU"; then
		if test "$("$ION_BIN_FIND" /dev/null -printf '%s' 2>/dev/null)" = 0; then
			export ION_BIN_FIND_GNU=1
		else
			export ION_BIN_FIND_GNU=0
		fi
	fi

	if test "$ION_BIN_XARGS" && ! test "$ION_BIN_XARGS_GNU"; then
		if "$ION_BIN_XARGS" -d"$NEWLINE" </dev/null 2>/dev/null; then
			export ION_BIN_XARGS_GNU=1
		else
			export ION_BIN_XARGS_GNU=0
		fi
	fi
}

init_check_commands() {
	init_check_posix || return
	init_check_bsd || return
	init_check_gnu || return

	init_check_command have_find "bfs or stat" || return
	init_check_command have_hash "openssl" || return
	init_check_command have_random "openssl" || return

	init_check_command can_build "xargs" || return
	init_check_command can_watch "fswatch" || return
	init_check_command can_serve "tcpserver and caddy" || return
}

init_check() {
	init_check_env || return
	init_check_commands || return
}

init_temp_sed() {
	if ! test "$TEMP_SED"; then
		TEMP_SED="$(start_temp_file sed)" || return
	fi
}

init_temp_blank() {
	if ! test "$TEMP_BLANK"; then
		TEMP_BLANK="$(start_temp_file blank md)" || return
	fi
}

init_temp_watch_lock() {
	if ! test "$TEMP_WATCH_LOCK"; then
		TEMP_WATCH_LOCK="$(start_temp_path watch-lock)" || return
	fi
}

init_temp_watch_stream() {
	if ! test "$TEMP_WATCH_STREAM"; then
		TEMP_WATCH_STREAM="$(start_temp_file watch-stream)" || return
	fi
}

init_temp_filter_empty() {
	if ! test "$TEMP_FILTER_EMPTY"; then
		TEMP_FILTER_EMPTY="$(start_temp_file filter-empty lua)" || return
		print "$SHARED_LUA" > "$TEMP_FILTER_EMPTY" || return
		print "$FILTER_EMPTY" >> "$TEMP_FILTER_EMPTY" || return
	fi
}

init_temp_filter_test() {
	if ! test "$TEMP_FILTER_TEST"; then
		TEMP_FILTER_TEST="$(start_temp_file filter-test lua)" || return
		print "$SHARED_LUA" > "$TEMP_FILTER_TEST" || return
		print "$FILTER_TEST" >> "$TEMP_FILTER_TEST" || return
	fi
}

init_temp_filter_split() {
	if ! test "$TEMP_FILTER_SPLIT"; then
		TEMP_FILTER_SPLIT="$(start_temp_file filter-split lua)" || return
		print "$SHARED_LUA" > "$TEMP_FILTER_SPLIT" || return
		print "$FILTER_SPLIT" >> "$TEMP_FILTER_SPLIT" || return
	fi
}

init_temp_filter_merge() {
	if ! test "$TEMP_FILTER_MERGE"; then
		TEMP_FILTER_MERGE="$(start_temp_file filter-merge lua)" || return
		print "$SHARED_LUA" > "$TEMP_FILTER_MERGE" || return
		print "$FILTER_MERGE" >> "$TEMP_FILTER_MERGE" || return
	fi
}

init_temp_filter_extract() {
	if ! test "$TEMP_FILTER_EXTRACT"; then
		TEMP_FILTER_EXTRACT="$(start_temp_file filter-extract lua)" || return
		print "$SHARED_LUA" > "$TEMP_FILTER_EXTRACT" || return
		print "$FILTER_EXTRACT" >> "$TEMP_FILTER_EXTRACT" || return
	fi
}

init_temp_filter_document() {
	if ! test "$TEMP_FILTER_DOCUMENT"; then
		TEMP_FILTER_DOCUMENT="$(start_temp_file filter-document lua)" || return
		print "$SHARED_LUA" > "$TEMP_FILTER_DOCUMENT" || return
		print "$FILTER_DOCUMENT" >> "$TEMP_FILTER_DOCUMENT" || return
	fi
}

init_temp_filter_template() {
	if ! test "$TEMP_FILTER_TEMPLATE"; then
		TEMP_FILTER_TEMPLATE="$(start_temp_file filter-template lua)" || return
		print "$SHARED_LUA" > "$TEMP_FILTER_TEMPLATE" || return
		print "$FILTER_TEMPLATE" >> "$TEMP_FILTER_TEMPLATE" || return
	fi
}

init_temp_template_json() {
	if ! test "$TEMP_TEMPLATE_JSON"; then
		TEMP_TEMPLATE_JSON="$(start_temp_file template-json template)" || return
		print "\$meta-json\$" > "$TEMP_TEMPLATE_JSON" || return
	fi
}

init_temp_template_html() {
	if ! test "$TEMP_TEMPLATE_HTML"; then
		TEMP_TEMPLATE_HTML="$(start_temp_file template-html template)" || return
		print "$TEMPLATE_HTML" > "$TEMP_TEMPLATE_HTML" || return
	fi
}

init_temp_source_style() {
	if test "$TEMP_SOURCE_STYLES"; then
		return
	fi

	TEMP_SOURCE_STYLES="$(start_temp_file src css)" || return

	if test "$ION_BUILD_CSS_GLOBAL"; then
		print "$GLOBAL_CSS" > "$TEMP_SOURCE_STYLES" || return
	fi

	paths_split_raw "$ION_SOURCE_STYLES" | {
		while IFS= read -r ek__path; do
			printf '@import "%s";\n' "$ek__path" >> "$TEMP_SOURCE_STYLES" || continue
		done
	}
}

init_temp_source_script() {
	if test "$TEMP_SOURCE_SCRIPTS"; then
		return
	fi

	TEMP_SOURCE_SCRIPTS="$(start_temp_file src js)" || return

	if test "$ION_BUILD_JS_GLOBAL"; then
		print "$GLOBAL_JS_ENV" > "$TEMP_SOURCE_SCRIPTS" || return
		print "$GLOBAL_JS" >> "$TEMP_SOURCE_SCRIPTS" || return
	fi

	paths_split_raw "$ION_SOURCE_SCRIPTS" | {
		el__i=0

		while IFS= read -r el__path; do
			el__i=$((el__i+1))

			printf 'import f%d from "%s";\n' "$el__i" "$el__path" >> "$TEMP_SOURCE_SCRIPTS" || continue
			printf 'typeof f%d === "function" && f%d();\n' "$el__i" "$el__i" >> "$TEMP_SOURCE_SCRIPTS" || continue
		done
	}

	if test "$ION_BUILD_JS_GLOBAL"; then
		printf 'components_start();\n' >> "$TEMP_SOURCE_SCRIPTS" || return
	fi
}

init_temp_compiled() {
	if ! test "$TEMP_COMPILED"; then
		TEMP_COMPILED="$(start_temp_dir compiled)" || return
	fi
}

init_temp_served() {
	if ! test "$ION_SERVED"; then
		dr__temp="$(start_temp_dir served)" || return
		export ION_SERVED="$dr__temp"
		SERVED_TEMP=1
	fi
}

init_temp_build() {
	if ! test "$ION_BUILD"; then
		ds__temp="$(start_temp_dir build)" || return
		export ION_BUILD="$ds__temp"
		BUILD_TEMP=1
	fi
}

init_temp() {
	init_temp_sed || return
	init_temp_blank || return
	init_temp_watch_lock || return
	init_temp_watch_stream || return
	init_temp_filter_empty || return
	init_temp_filter_test || return
	init_temp_filter_split || return
	init_temp_filter_merge || return
	init_temp_filter_extract || return
	init_temp_filter_document || return
	init_temp_filter_template || return
	init_temp_template_json || return
	init_temp_template_html || return
	init_temp_source_style || return
	init_temp_source_script || return
	init_temp_compiled || return
	init_temp_served || return
	init_temp_build || return
}

init_parent() {
	init_env || return
	init_check || return
	init_temp || return
}

init() {
	init_signals || return
	init_basics || return

	if ! have_parent && ! test "$STARTED"; then
		STARTED=1
		init_parent "$@" || return
	fi

	if have_parent && test "$ION_START_ID" = 1; then
		ION_START_ID=$$
	fi
}

test_self() {
	if have_shellcheck; then
		start "$ION_BIN_SHELLCHECK" "$ION_BIN_SELF"
	fi
}

test_scripts() {
	if have_luac; then
		start "$ION_BIN_LUAC" -p \
			"$TEMP_FILTER_EMPTY" \
			"$TEMP_FILTER_TEST" \
			"$TEMP_FILTER_SPLIT" \
			"$TEMP_FILTER_MERGE" \
			"$TEMP_FILTER_EXTRACT" \
			"$TEMP_FILTER_DOCUMENT" \
			"$TEMP_FILTER_TEMPLATE"
	fi
}

test_all() {
	note "$ION__MSG_RUNNING_TESTS"

	test_self || return
	test_scripts || return
	start_pandoc_tests || return
}

usage() {
	echo hello world
}

main() {
	init "$@" || exit 1

	if should_help && ! usage; then
		exit 2
	elif should_build && ! start_building "$@"; then
		exit 3
	elif ! have_parent; then
		if should_test && ! test_all; then
			exit 4
		fi

		if should_serve && ! start_server; then
			exit 5
		fi

		if should_watch && ! start_watcher; then
			exit 6
		fi

		if ! start_builder; then
			exit 7
		fi
	fi
}

main "$@"